Compare commits

..

4 Commits

Author SHA1 Message Date
Aria Desires
97af9d8466 fmt 2025-07-07 12:46:33 -04:00
Aria Desires
5cc6762c23 fixup 2025-07-03 19:20:47 -04:00
Aria Desires
1bee527cb2 add many tests 2025-07-03 19:20:47 -04:00
Aria Desires
d7b7b835e1 Add initial implementation of goto definition for loads of local names 2025-07-03 19:20:44 -04:00
135 changed files with 2437 additions and 6144 deletions

View File

@@ -214,7 +214,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -234,17 +234,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -292,17 +292,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: "Run tests"
@@ -321,11 +321,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -348,7 +348,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -377,7 +377,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -400,7 +400,7 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
@@ -408,11 +408,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: "Run tests"
@@ -432,7 +432,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
@@ -494,7 +494,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -708,7 +708,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
@@ -732,7 +732,7 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
@@ -765,7 +765,7 @@ jobs:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
@@ -804,7 +804,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup show
- name: "Run checks"
@@ -874,7 +874,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
@@ -905,14 +905,14 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-codspeed
@@ -938,14 +938,14 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-codspeed

View File

@@ -39,7 +39,7 @@ jobs:
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- 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

View File

@@ -39,7 +39,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
@@ -49,12 +49,46 @@ jobs:
- name: Run mypy_primer
shell: bash
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
DIFF_FILE: mypy_primer.diff
TY_MEMORY_REPORT: mypy_primer
run: |
cd ruff
scripts/mypy_primer.sh
echo ${{ github.event.number }} > ../pr-number
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b base_commit "$MERGE_BASE"
echo "base commit"
git rev-list --format=%s --max-count=1 base_commit
cd ..
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
mypy_primer \
--repo ruff \
--type-checker ty \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector "/($PRIMER_SELECTOR)\$" \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]
# Output diff with ANSI color codes
cat mypy_primer.diff
# Remove ANSI color codes before uploading
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -67,41 +101,3 @@ jobs:
with:
name: pr-number
path: pr-number
memory_usage:
name: Run memory statistics
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Run mypy_primer
shell: bash
env:
TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
TY_MEMORY_REPORT: mypy_primer
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/memory.txt
DIFF_FILE: mypy_primer_memory.diff
run: |
cd ruff
scripts/mypy_primer.sh
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mypy_primer_memory_diff
path: mypy_primer_memory.diff

View File

@@ -45,28 +45,15 @@ jobs:
if_no_artifact_found: ignore
allow_forks: true
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download mypy_primer memory results"
id: download-mypy_primer_memory_diff
if: steps.pr-number.outputs.pr-number
with:
name: mypy_primer_memory_diff
workflow: mypy_primer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/mypy_primer_memory_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: ${{ steps.download-mypy_primer_diff.outputs.found_artifact == 'true' && steps.download-mypy_primer_memory_diff.outputs.found_artifact == 'true' }}
if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
run: |
# Guard against malicious mypy_primer results that symlink to a secret
# file on this runner
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] || [[ -L pr/mypy_primer_memory_diff/mypy_primer_memory.diff ]]
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
then
echo "Error: mypy_primer.diff and mypy_primer_memory.diff cannot be a symlink"
echo "Error: mypy_primer.diff cannot be a symlink"
exit 1
fi
@@ -87,18 +74,6 @@ jobs:
echo 'No ecosystem changes detected ✅' >> comment.txt
fi
if [ -s "pr/mypy_primer_memory_diff/mypy_primer_memory.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Memory usage changes were detected when running on open source projects</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/mypy_primer_memory_diff/mypy_primer_memory.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No memory usage changes detected ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"

View File

@@ -68,7 +68,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"

View File

@@ -67,7 +67,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.34.0
rev: v1.33.1
hooks:
- id: typos
@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2
rev: v0.12.1
hooks:
- id: ruff-format
- id: ruff
@@ -99,12 +99,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.11.0
rev: v1.10.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.2
rev: 0.33.1
hooks:
- id: check-github-workflows

171
Cargo.lock generated
View File

@@ -591,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -600,7 +600,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -629,24 +629,12 @@ name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "console"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1031,7 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1493,14 +1481,14 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.18.0"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console 0.16.0",
"console",
"number_prefix",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
"vt100",
"web-time",
]
@@ -1537,7 +1525,7 @@ version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console 0.15.11",
"console",
"globset",
"once_cell",
"pest",
@@ -1604,7 +1592,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.1",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1668,7 +1656,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2037,11 +2025,12 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
version = "8.1.0"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [
"bitflags 2.9.1",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2050,7 +2039,7 @@ dependencies = [
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -2088,6 +2077,12 @@ dependencies = [
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -2171,7 +2166,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@@ -3412,7 +3407,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3608,9 +3603,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.14.0"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"serde",
"serde_derive",
@@ -3619,9 +3614,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.14.0"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
@@ -3802,7 +3797,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4061,9 +4056,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -4082,9 +4077,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.11"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200"
checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1"
dependencies = [
"indicatif",
"tracing",
@@ -4172,7 +4167,6 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"insta",
"ruff_db",
"ruff_python_ast",
@@ -4479,12 +4473,6 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "unit-prefix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
[[package]]
name = "unscanny"
version = "0.1.0"
@@ -4817,7 +4805,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4891,7 +4879,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@@ -4900,16 +4888,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
"windows-targets",
]
[[package]]
@@ -4918,30 +4897,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
@@ -4950,96 +4913,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.10"

View File

@@ -98,7 +98,7 @@ ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indexmap = { version = "2.6.0" }
indicatif = { version = "0.18.0" }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
@@ -167,7 +167,7 @@ tikv-jemallocator = { version = "0.6.0" }
toml = { version = "0.8.11" }
tracing = { version = "0.1.40" }
tracing-flame = { version = "0.2.0" }
tracing-indicatif = { version = "0.3.11" }
tracing-indicatif = { version = "0.3.6" }
tracing-log = { version = "0.2.0" }
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"env-filter",

View File

@@ -506,7 +506,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Streamlit](https://github.com/streamlit/streamlit)
- [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair)
- [Weblate](https://weblate.org/)
- WordPress ([Openverse](https://github.com/WordPress/openverse))
- [ZenML](https://github.com/zenml-io/zenml)
- [Zulip](https://github.com/zulip/zulip)

View File

@@ -498,8 +498,11 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
let diagnostics = result.len();
assert!(
diagnostics <= max_diagnostics,
"Expected <={max_diagnostics} diagnostics but got {diagnostics}"
diagnostics > 1 && diagnostics <= max_diagnostics,
"Expected between {} and {} diagnostics but got {}",
1,
max_diagnostics,
diagnostics
);
}
@@ -567,23 +570,6 @@ fn anyio(criterion: &mut Criterion) {
bench_project(&benchmark, criterion);
}
fn datetype(criterion: &mut Criterion) {
let benchmark = ProjectBenchmark::new(
RealWorldProject {
name: "DateType",
repository: "https://github.com/glyph/DateType",
commit: "57c9c93cf2468069f72945fc04bf27b64100dad8",
paths: vec![SystemPath::new("src")],
dependencies: vec![],
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
0,
);
bench_project(&benchmark, criterion);
}
criterion_group!(check_file, benchmark_cold, benchmark_incremental);
criterion_group!(
micro,
@@ -592,5 +578,5 @@ criterion_group!(
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
);
criterion_group!(project, anyio, attrs, hydra, datetype);
criterion_group!(project, anyio, attrs, hydra);
criterion_main!(check_file, micro, project);

View File

@@ -242,19 +242,20 @@ fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
// Currently disabled because the benchmark is too noisy (± 10%) to give useful feedback.
// #[bench(args=[&*PYDANTIC], sample_size=3, sample_count=3)]
// fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
// let thread_pool = ThreadPoolBuilder::new().build().unwrap();
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.max_diagnostics);
db
})
});
}
// bencher
// .with_inputs(|| benchmark.setup_iteration())
// .bench_local_values(|db| {
// thread_pool.install(|| {
// check_project(&db, benchmark.max_diagnostics);
// db
// })
// });
// }
fn main() {
ThreadPoolBuilder::new()

View File

@@ -1,30 +1,29 @@
for _ in []:
try:
pass
except Exception:
continue
try:
pass
except Exception:
continue
try:
pass
except:
continue
try:
pass
except:
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except ValueError:
continue
try:
pass
except ValueError:
continue
try:
pass
except (ValueError,):
continue
try:
pass
except (ValueError,):
continue

View File

@@ -185,45 +185,38 @@ for _section, section_items in groupby(items, key=lambda p: p[1]):
collect_shop_items(shopper, section_items)
# Shouldn't trigger the warning when there is a return statement.
def foo():
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
collect_shop_items(shopper, section_items)
# Should trigger the warning for duplicate access, even if is a return statement after.
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
# Should trigger the warning for duplicate access, even if is a return in another branch.
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
# Should trigger, since only one branch has a return statement.
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
# Let's redefine the `groupby` function to make sure we pick up the correct one.
# NOTE: This should always be at the end of the file.

View File

@@ -26,9 +26,8 @@ abc(**{'a': b}, **{'a': c}) # PIE804
abc(a=1, **{'a': c}, **{'b': c}) # PIE804
# Some values need to be parenthesized.
def foo():
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804
# https://github.com/astral-sh/ruff/issues/18036
# The autofix for this is unsafe due to the comments inside the dictionary.

View File

@@ -27,9 +27,8 @@ with contextlib.ExitStack() as stack:
close_files = stack.pop_all().close
# OK
async def foo():
with contextlib.AsyncExitStack() as exit_stack:
f = await exit_stack.enter_async_context(open("filename"))
with contextlib.AsyncExitStack() as exit_stack:
f = await exit_stack.enter_async_context(open("filename"))
# OK (false negative)
with contextlib.ExitStack():
@@ -276,10 +275,9 @@ class ExampleClassTests(TestCase):
cls.enterClassContext(open("filename"))
# OK
async def foo():
class ExampleAsyncTests(IsolatedAsyncioTestCase):
async def test_something(self):
await self.enterAsyncContext(open("filename"))
class ExampleAsyncTests(IsolatedAsyncioTestCase):
async def test_something(self):
await self.enterAsyncContext(open("filename"))
# OK
class ExampleTests(TestCase):

View File

@@ -1,99 +1,98 @@
def foo():
# Errors
a = "hello"
# Errors
a = "hello"
# SIM116
if a == "foo":
return "bar"
elif a == "bar":
return "baz"
elif a == "boo":
return "ooh"
# SIM116
if a == "foo":
return "bar"
elif a == "bar":
return "baz"
elif a == "boo":
return "ooh"
else:
return 42
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
# SIM116
if a == "hello 'sir'":
return (1, 2, 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == b"one":
return 1
elif a == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if a == "hello 'sir'":
return ("hello'", 'hi"', 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# OK
if a == "foo":
return "bar"
elif a == "bar":
return baz()
elif a == "boo":
return "ooh"
else:
return 42
# OK
if a == b"one":
return 1
elif b == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if func_name == "create":
return "A"
elif func_name == "modify":
return "M"
elif func_name == "remove":
return "D"
elif func_name == "move":
return "MV"
# OK
def no_return_in_else(platform):
if platform == "linux":
return "auditwheel repair -w {dest_dir} {wheel}"
elif platform == "macos":
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
elif platform == "windows":
return ""
else:
return 42
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
# SIM116
if a == "hello 'sir'":
return (1, 2, 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == b"one":
return 1
elif a == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if a == "hello 'sir'":
return ("hello'", 'hi"', 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# OK
if a == "foo":
return "bar"
elif a == "bar":
return baz()
elif a == "boo":
return "ooh"
else:
return 42
# OK
if a == b"one":
return 1
elif b == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if func_name == "create":
return "A"
elif func_name == "modify":
return "M"
elif func_name == "remove":
return "D"
elif func_name == "move":
return "MV"
# OK
def no_return_in_else(platform):
if platform == "linux":
return "auditwheel repair -w {dest_dir} {wheel}"
elif platform == "macos":
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
elif platform == "windows":
return ""
else:
msg = f"Unknown platform: {platform!r}"
raise ValueError(msg)
msg = f"Unknown platform: {platform!r}"
raise ValueError(msg)

View File

@@ -50,23 +50,3 @@ class Baz:
class Nested:
a: TypeAlias = 'Baz' # OK
type A = 'Baz' # TC008
# O should have parenthesis added
o: TypeAlias = """int
| None"""
type O = """int
| None"""
# P, Q, and R should not have parenthesis added
p: TypeAlias = ("""int
| None""")
type P = ("""int
| None""")
q: TypeAlias = """(int
| None)"""
type Q = """(int
| None)"""
r: TypeAlias = """int | None"""
type R = """int | None"""

View File

@@ -1,4 +1,4 @@
import os.path, pathlib
import os.path
from pathlib import Path
from os.path import getatime
@@ -10,26 +10,3 @@ os.path.getatime(Path("filename"))
getatime("filename")
getatime(b"filename")
getatime(Path("filename"))
file = __file__
os.path.getatime(file)
os.path.getatime(filename="filename")
os.path.getatime(filename=Path("filename"))
os.path.getatime( # comment 1
# comment 2
"filename" # comment 3
# comment 4
, # comment 5
# comment 6
) # comment 7
os.path.getatime("file" + "name")
getatime(Path("filename").resolve())
os.path.getatime(pathlib.Path("filename"))
getatime(Path("dir") / "file.txt")

View File

@@ -81,5 +81,4 @@ match(foo):
# https://github.com/astral-sh/ruff/issues/12094
pass;
def foo():
yield, x
yield, x

View File

@@ -1062,6 +1062,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::OsPathGetatime,
Rule::OsPathGetmtime,
Rule::OsPathGetctime,
Rule::Glob,
Rule::OsListdir,
Rule::OsSymlink,
@@ -1071,15 +1074,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
}

View File

@@ -54,20 +54,6 @@ pub(crate) const fn is_fix_manual_list_comprehension_enabled(settings: &LinterSe
pub(crate) const fn is_fix_os_path_getsize_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getmtime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getatime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168

View File

@@ -1,46 +1,46 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S112.py:4:5: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:3:1: S112 `try`-`except`-`continue` detected, consider logging the exception
|
2 | try:
3 | pass
4 | / except Exception:
5 | | continue
| |________________^ S112
6 |
7 | try:
1 | try:
2 | pass
3 | / except Exception:
4 | | continue
| |____________^ S112
5 |
6 | try:
|
S112.py:9:5: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:8:1: S112 `try`-`except`-`continue` detected, consider logging the exception
|
7 | try:
8 | pass
9 | / except:
10 | | continue
| |________________^ S112
11 |
12 | try:
6 | try:
7 | pass
8 | / except:
9 | | continue
| |____________^ S112
10 |
11 | try:
|
S112.py:14:5: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:13:1: S112 `try`-`except`-`continue` detected, consider logging the exception
|
12 | try:
13 | pass
14 | / except (Exception,):
15 | | continue
| |________________^ S112
16 |
17 | try:
11 | try:
12 | pass
13 | / except (Exception,):
14 | | continue
| |____________^ S112
15 |
16 | try:
|
S112.py:19:5: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:18:1: S112 `try`-`except`-`continue` detected, consider logging the exception
|
17 | try:
18 | pass
19 | / except (Exception, ValueError):
20 | | continue
| |________________^ S112
21 |
22 | try:
16 | try:
17 | pass
18 | / except (Exception, ValueError):
19 | | continue
| |____________^ S112
20 |
21 | try:
|

View File

@@ -195,31 +195,31 @@ B031.py:144:33: B031 Using the generator returned from `itertools.groupby()` mor
146 | for group in groupby(items, key=lambda p: p[1]):
|
B031.py:203:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
B031.py:200:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
201 | if _section == "greens":
202 | collect_shop_items(shopper, section_items)
203 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
204 | return
|
B031.py:215:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
213 | elif _section == "frozen items":
214 | collect_shop_items(shopper, section_items)
215 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
216 |
217 | # Should trigger, since only one branch has a return statement.
|
B031.py:226:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
224 | elif _section == "frozen items":
225 | collect_shop_items(shopper, section_items)
226 | collect_shop_items(shopper, section_items) # B031
198 | if _section == "greens":
199 | collect_shop_items(shopper, section_items)
200 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
227 |
228 | # Let's redefine the `groupby` function to make sure we pick up the correct one.
201 | return
|
B031.py:210:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
208 | elif _section == "frozen items":
209 | collect_shop_items(shopper, section_items)
210 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
211 |
212 | # Should trigger, since only one branch has a return statement.
|
B031.py:219:33: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
217 | elif _section == "frozen items":
218 | collect_shop_items(shopper, section_items)
219 | collect_shop_items(shopper, section_items) # B031
| ^^^^^^^^^^^^^ B031
220 |
221 | # Let's redefine the `groupby` function to make sure we pick up the correct one.
|

View File

@@ -190,73 +190,72 @@ PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
26 |+abc(a=1, **{'a': c}, b=c) # PIE804
27 27 |
28 28 | # Some values need to be parenthesized.
29 29 | def foo():
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
PIE804.py:30:16: PIE804 [*] Unnecessary `dict` kwargs
PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs
|
28 | # Some values need to be parenthesized.
29 | def foo():
30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
= help: Remove unnecessary kwargs
Safe fix
26 26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
27 27 |
28 28 | # Some values need to be parenthesized.
29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804
29 |+abc(foo=1, bar=(bar := 1)) # PIE804
30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
28 | # Some values need to be parenthesized.
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^ PIE804
31 |
32 | # https://github.com/astral-sh/ruff/issues/18036
|
= help: Remove unnecessary kwargs
Safe fix
27 27 |
28 28 | # Some values need to be parenthesized.
29 29 | def foo():
30 |- abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 |+ abc(foo=1, bar=(bar := 1)) # PIE804
31 31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
32 32 |
33 33 | # https://github.com/astral-sh/ruff/issues/18036
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804
30 |+abc(foo=1, bar=(yield 1)) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
PIE804.py:31:16: PIE804 [*] Unnecessary `dict` kwargs
PIE804.py:35:5: PIE804 [*] Unnecessary `dict` kwargs
|
29 | def foo():
30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^ PIE804
32 |
33 | # https://github.com/astral-sh/ruff/issues/18036
|
= help: Remove unnecessary kwargs
Safe fix
28 28 | # Some values need to be parenthesized.
29 29 | def foo():
30 30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
31 |- abc(foo=1, **{'bar': (yield 1)}) # PIE804
31 |+ abc(foo=1, bar=(yield 1)) # PIE804
32 32 |
33 33 | # https://github.com/astral-sh/ruff/issues/18036
34 34 | # The autofix for this is unsafe due to the comments inside the dictionary.
PIE804.py:36:5: PIE804 [*] Unnecessary `dict` kwargs
|
34 | # The autofix for this is unsafe due to the comments inside the dictionary.
35 | foo(
36 | / **{
37 | | # Comment 1
38 | | "x": 1.0,
39 | | # Comment 2
40 | | "y": 2.0,
41 | | }
33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 | foo(
35 | / **{
36 | | # Comment 1
37 | | "x": 1.0,
38 | | # Comment 2
39 | | "y": 2.0,
40 | | }
| |_____^ PIE804
42 | )
41 | )
|
= help: Remove unnecessary kwargs
Unsafe fix
33 33 | # https://github.com/astral-sh/ruff/issues/18036
34 34 | # The autofix for this is unsafe due to the comments inside the dictionary.
35 35 | foo(
36 |- **{
37 |- # Comment 1
38 |- "x": 1.0,
39 |- # Comment 2
40 |- "y": 2.0,
41 |- }
36 |+ x=1.0, y=2.0
42 37 | )
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 34 | foo(
35 |- **{
36 |- # Comment 1
37 |- "x": 1.0,
38 |- # Comment 2
39 |- "y": 2.0,
40 |- }
35 |+ x=1.0, y=2.0
41 36 | )

View File

@@ -21,9 +21,7 @@ use crate::registry::Rule;
///
/// ## Example
/// ```pyi
/// import sys
///
/// if sys.platform == "xunil"[::-1]:
/// if sys.platform.startswith("linux"):
/// # Linux specific definitions
/// ...
/// else:
@@ -33,8 +31,6 @@ use crate::registry::Rule;
///
/// Instead, use a simple string comparison, such as `==` or `!=`:
/// ```pyi
/// import sys
///
/// if sys.platform == "linux":
/// # Linux specific definitions
/// ...
@@ -69,15 +65,11 @@ impl Violation for UnrecognizedPlatformCheck {
///
/// ## Example
/// ```pyi
/// import sys
///
/// if sys.platform == "linus": ...
/// ```
///
/// Use instead:
/// ```pyi
/// import sys
///
/// if sys.platform == "linux": ...
/// ```
///

View File

@@ -18,22 +18,17 @@ use crate::checkers::ast::Checker;
///
/// ## Example
/// ```python
/// def find_phrase(x):
/// if x == 1:
/// return "Hello"
/// elif x == 2:
/// return "Goodbye"
/// elif x == 3:
/// return "Good morning"
/// else:
/// return "Goodnight"
/// if x == 1:
/// return "Hello"
/// elif x == 2:
/// return "Goodbye"
/// else:
/// return "Goodnight"
/// ```
///
/// Use instead:
/// ```python
/// def find_phrase(x):
/// phrases = {1: "Hello", 2: "Goodye", 3: "Good morning"}
/// return phrases.get(x, "Goodnight")
/// return {1: "Hello", 2: "Goodbye"}.get(x, "Goodnight")
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct IfElseBlockInsteadOfDictLookup;

View File

@@ -50,285 +50,285 @@ SIM115.py:12:5: SIM115 Use a context manager for opening files
14 | f.close()
|
SIM115.py:40:9: SIM115 Use a context manager for opening files
SIM115.py:39:9: SIM115 Use a context manager for opening files
|
38 | # SIM115
39 | with contextlib.ExitStack():
40 | f = open("filename")
37 | # SIM115
38 | with contextlib.ExitStack():
39 | f = open("filename")
| ^^^^ SIM115
41 |
42 | # OK
40 |
41 | # OK
|
SIM115.py:80:5: SIM115 Use a context manager for opening files
|
78 | import fileinput
79 |
80 | f = tempfile.NamedTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
|
SIM115.py:81:5: SIM115 Use a context manager for opening files
|
79 | import fileinput
80 |
81 | f = tempfile.NamedTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
80 | f = tempfile.NamedTemporaryFile()
81 | f = tempfile.TemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
|
SIM115.py:82:5: SIM115 Use a context manager for opening files
|
81 | f = tempfile.NamedTemporaryFile()
82 | f = tempfile.TemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
80 | f = tempfile.NamedTemporaryFile()
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
|
SIM115.py:83:5: SIM115 Use a context manager for opening files
|
81 | f = tempfile.NamedTemporaryFile()
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
| ^^^^^^^^^^^^ SIM115
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
|
SIM115.py:84:5: SIM115 Use a context manager for opening files
|
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
| ^^^^^^^^^^^^ SIM115
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
|
SIM115.py:85:5: SIM115 Use a context manager for opening files
|
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
|
SIM115.py:86:5: SIM115 Use a context manager for opening files
|
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
|
SIM115.py:87:5: SIM115 Use a context manager for opening files
|
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
|
SIM115.py:88:5: SIM115 Use a context manager for opening files
|
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
| ^^^^^^^ SIM115
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
|
SIM115.py:89:5: SIM115 Use a context manager for opening files
|
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
| ^^^^^^^ SIM115
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
| ^^^^^^^^^^^^ SIM115
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
|
SIM115.py:90:5: SIM115 Use a context manager for opening files
|
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
| ^^^^^^^^^^^^ SIM115
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
| ^^^^^^^^^^^ SIM115
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
|
SIM115.py:91:5: SIM115 Use a context manager for opening files
|
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
| ^^^^^^^^^^^ SIM115
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
| ^^^^^^^^ SIM115
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
|
SIM115.py:92:5: SIM115 Use a context manager for opening files
|
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
| ^^^^^^^^ SIM115
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
| ^^^^^^^^^ SIM115
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
|
SIM115.py:93:5: SIM115 Use a context manager for opening files
|
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
| ^^^^^^^^^ SIM115
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
| ^^^^^^^^ SIM115
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
|
SIM115.py:94:5: SIM115 Use a context manager for opening files
|
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
| ^^^^^^^^ SIM115
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
| ^^^^^^^^^^^^ SIM115
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
|
SIM115.py:95:5: SIM115 Use a context manager for opening files
|
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
| ^^^^^^^^^^^^ SIM115
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
|
SIM115.py:96:5: SIM115 Use a context manager for opening files
|
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
|
SIM115.py:97:5: SIM115 Use a context manager for opening files
|
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
| ^^^^^^^^^ SIM115
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
|
SIM115.py:98:5: SIM115 Use a context manager for opening files
|
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
| ^^^^^^^^^ SIM115
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
| ^^^^^^^^^^^^^ SIM115
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
|
SIM115.py:99:5: SIM115 Use a context manager for opening files
|
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
| ^^^^^^^^^^^^^ SIM115
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
| ^^^^^^^^^^^ SIM115
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
|
SIM115.py:100:5: SIM115 Use a context manager for opening files
|
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
| ^^^^^^^^^^^ SIM115
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
| ^^^^^^^^^^^^^ SIM115
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
|
SIM115.py:101:5: SIM115 Use a context manager for opening files
|
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
| ^^^^^^^^^^^^^ SIM115
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
| ^^^^^^^^^ SIM115
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
|
SIM115.py:102:5: SIM115 Use a context manager for opening files
|
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
| ^^^^^^^^^ SIM115
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
103 | f = fileinput.input("foo.txt")
104 | f = fileinput.FileInput("foo.txt")
|
SIM115.py:103:5: SIM115 Use a context manager for opening files
|
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
104 | f = fileinput.input("foo.txt")
105 | f = fileinput.FileInput("foo.txt")
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
| ^^^^^^^^^^^^^^^ SIM115
104 | f = fileinput.FileInput("foo.txt")
|
SIM115.py:104:5: SIM115 Use a context manager for opening files
|
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
| ^^^^^^^^^^^^^^^ SIM115
105 | f = fileinput.FileInput("foo.txt")
|
SIM115.py:105:5: SIM115 Use a context manager for opening files
|
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
105 | f = fileinput.FileInput("foo.txt")
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
104 | f = fileinput.FileInput("foo.txt")
| ^^^^^^^^^^^^^^^^^^^ SIM115
106 |
107 | with contextlib.suppress(Exception):
105 |
106 | with contextlib.suppress(Exception):
|
SIM115.py:241:9: SIM115 Use a context manager for opening files
SIM115.py:240:9: SIM115 Use a context manager for opening files
|
239 | def aliased():
240 | from shelve import open as open_shelf
241 | x = open_shelf("foo.dbm")
238 | def aliased():
239 | from shelve import open as open_shelf
240 | x = open_shelf("foo.dbm")
| ^^^^^^^^^^ SIM115
242 | x.close()
241 | x.close()
|
SIM115.py:245:9: SIM115 Use a context manager for opening files
SIM115.py:244:9: SIM115 Use a context manager for opening files
|
244 | from tarfile import TarFile as TF
245 | f = TF("foo").open()
243 | from tarfile import TarFile as TF
244 | f = TF("foo").open()
| ^^^^^^^^^^^^^^ SIM115
246 | f.close()
245 | f.close()
|
SIM115.py:258:5: SIM115 Use a context manager for opening files
SIM115.py:257:5: SIM115 Use a context manager for opening files
|
257 | # SIM115
258 | f = dbm.sqlite3.open("foo.db")
256 | # SIM115
257 | f = dbm.sqlite3.open("foo.db")
| ^^^^^^^^^^^^^^^^ SIM115
259 | f.close()
258 | f.close()
|

View File

@@ -1,110 +1,110 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM116.py:6:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:5:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
5 | # SIM116
6 | / if a == "foo":
7 | | return "bar"
8 | | elif a == "bar":
9 | | return "baz"
10 | | elif a == "boo":
11 | | return "ooh"
12 | | else:
13 | | return 42
| |_________________^ SIM116
14 |
15 | # SIM116
4 | # SIM116
5 | / if a == "foo":
6 | | return "bar"
7 | | elif a == "bar":
8 | | return "baz"
9 | | elif a == "boo":
10 | | return "ooh"
11 | | else:
12 | | return 42
| |_____________^ SIM116
13 |
14 | # SIM116
|
SIM116.py:16:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:15:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
15 | # SIM116
16 | / if a == 1:
17 | | return (1, 2, 3)
18 | | elif a == 2:
19 | | return (4, 5, 6)
20 | | elif a == 3:
21 | | return (7, 8, 9)
22 | | else:
23 | | return (10, 11, 12)
| |___________________________^ SIM116
24 |
25 | # SIM116
14 | # SIM116
15 | / if a == 1:
16 | | return (1, 2, 3)
17 | | elif a == 2:
18 | | return (4, 5, 6)
19 | | elif a == 3:
20 | | return (7, 8, 9)
21 | | else:
22 | | return (10, 11, 12)
| |_______________________^ SIM116
23 |
24 | # SIM116
|
SIM116.py:26:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:25:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
25 | # SIM116
26 | / if a == 1:
27 | | return (1, 2, 3)
28 | | elif a == 2:
29 | | return (4, 5, 6)
30 | | elif a == 3:
31 | | return (7, 8, 9)
| |________________________^ SIM116
32 |
33 | # SIM116
24 | # SIM116
25 | / if a == 1:
26 | | return (1, 2, 3)
27 | | elif a == 2:
28 | | return (4, 5, 6)
29 | | elif a == 3:
30 | | return (7, 8, 9)
| |____________________^ SIM116
31 |
32 | # SIM116
|
SIM116.py:34:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:33:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
33 | # SIM116
34 | / if a == "hello 'sir'":
35 | | return (1, 2, 3)
36 | | elif a == 'goodbye "mam"':
37 | | return (4, 5, 6)
38 | | elif a == """Fairwell 'mister'""":
39 | | return (7, 8, 9)
40 | | else:
41 | | return (10, 11, 12)
| |___________________________^ SIM116
42 |
43 | # SIM116
32 | # SIM116
33 | / if a == "hello 'sir'":
34 | | return (1, 2, 3)
35 | | elif a == 'goodbye "mam"':
36 | | return (4, 5, 6)
37 | | elif a == """Fairwell 'mister'""":
38 | | return (7, 8, 9)
39 | | else:
40 | | return (10, 11, 12)
| |_______________________^ SIM116
41 |
42 | # SIM116
|
SIM116.py:44:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:43:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
43 | # SIM116
44 | / if a == b"one":
45 | | return 1
46 | | elif a == b"two":
47 | | return 2
48 | | elif a == b"three":
49 | | return 3
| |________________^ SIM116
50 |
51 | # SIM116
42 | # SIM116
43 | / if a == b"one":
44 | | return 1
45 | | elif a == b"two":
46 | | return 2
47 | | elif a == b"three":
48 | | return 3
| |____________^ SIM116
49 |
50 | # SIM116
|
SIM116.py:52:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:51:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
51 | # SIM116
52 | / if a == "hello 'sir'":
53 | | return ("hello'", 'hi"', 3)
54 | | elif a == 'goodbye "mam"':
55 | | return (4, 5, 6)
56 | | elif a == """Fairwell 'mister'""":
57 | | return (7, 8, 9)
58 | | else:
59 | | return (10, 11, 12)
| |___________________________^ SIM116
60 |
61 | # OK
50 | # SIM116
51 | / if a == "hello 'sir'":
52 | | return ("hello'", 'hi"', 3)
53 | | elif a == 'goodbye "mam"':
54 | | return (4, 5, 6)
55 | | elif a == """Fairwell 'mister'""":
56 | | return (7, 8, 9)
57 | | else:
58 | | return (10, 11, 12)
| |_______________________^ SIM116
59 |
60 | # OK
|
SIM116.py:80:5: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:79:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
79 | # SIM116
80 | / if func_name == "create":
81 | | return "A"
82 | | elif func_name == "modify":
83 | | return "M"
84 | | elif func_name == "remove":
85 | | return "D"
86 | | elif func_name == "move":
87 | | return "MV"
| |___________________^ SIM116
88 |
89 | # OK
78 | # SIM116
79 | / if func_name == "create":
80 | | return "A"
81 | | elif func_name == "modify":
82 | | return "M"
83 | | elif func_name == "remove":
84 | | return "D"
85 | | elif func_name == "move":
86 | | return "MV"
| |_______________^ SIM116
87 |
88 | # OK
|

View File

@@ -11,7 +11,6 @@ use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
use ruff_python_ast::parenthesize::parenthesized_range;
/// ## What it does
/// Checks if [PEP 613] explicit type aliases contain references to
@@ -88,15 +87,11 @@ impl Violation for UnquotedTypeAlias {
/// ## Example
/// Given:
/// ```python
/// from typing import TypeAlias
///
/// OptInt: TypeAlias = "int | None"
/// ```
///
/// Use instead:
/// ```python
/// from typing import TypeAlias
///
/// OptInt: TypeAlias = int | None
/// ```
///
@@ -292,30 +287,7 @@ pub(crate) fn quoted_type_alias(
let range = annotation_expr.range();
let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range);
let fix_string = annotation_expr.value.to_string();
let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r'))
&& parenthesized_range(
// Check for parenthesis outside string ("""...""")
annotation_expr.into(),
checker.semantic().current_statement().into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.is_none()
&& parenthesized_range(
// Check for parenthesis inside string """(...)"""
expr.into(),
annotation_expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.is_none()
{
format!("({fix_string})")
} else {
fix_string
};
let edit = Edit::range_replacement(fix_string, range);
let edit = Edit::range_replacement(annotation_expr.value.to_string(), range);
if checker.comment_ranges().intersects(range) {
diagnostic.set_fix(Fix::unsafe_edit(edit));
} else {

View File

@@ -44,7 +44,7 @@ use crate::{Fix, FixAvailability, Violation};
/// ```python
/// from __future__ import annotations
///
/// from . import local_module
/// import local_module
///
///
/// def func(sized: local_module.Container) -> int:
@@ -58,7 +58,7 @@ use crate::{Fix, FixAvailability, Violation};
/// from typing import TYPE_CHECKING
///
/// if TYPE_CHECKING:
/// from . import local_module
/// import local_module
///
///
/// def func(sized: local_module.Container) -> int:

View File

@@ -409,8 +409,6 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias
51 | a: TypeAlias = 'Baz' # OK
52 | type A = 'Baz' # TC008
| ^^^^^ TC008
53 |
54 | # O should have parenthesis added
|
= help: Remove quotes
@@ -420,187 +418,3 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias
51 51 | a: TypeAlias = 'Baz' # OK
52 |- type A = 'Baz' # TC008
52 |+ type A = Baz # TC008
53 53 |
54 54 | # O should have parenthesis added
55 55 | o: TypeAlias = """int
TC008.py:55:16: TC008 [*] Remove quotes from type alias
|
54 | # O should have parenthesis added
55 | o: TypeAlias = """int
| ________________^
56 | | | None"""
| |_________^ TC008
57 | type O = """int
58 | | None"""
|
= help: Remove quotes
Safe fix
52 52 | type A = 'Baz' # TC008
53 53 |
54 54 | # O should have parenthesis added
55 |-o: TypeAlias = """int
56 |-| None"""
55 |+o: TypeAlias = (int
56 |+| None)
57 57 | type O = """int
58 58 | | None"""
59 59 |
TC008.py:57:10: TC008 [*] Remove quotes from type alias
|
55 | o: TypeAlias = """int
56 | | None"""
57 | type O = """int
| __________^
58 | | | None"""
| |_________^ TC008
59 |
60 | # P, Q, and R should not have parenthesis added
|
= help: Remove quotes
Safe fix
54 54 | # O should have parenthesis added
55 55 | o: TypeAlias = """int
56 56 | | None"""
57 |-type O = """int
58 |-| None"""
57 |+type O = (int
58 |+| None)
59 59 |
60 60 | # P, Q, and R should not have parenthesis added
61 61 | p: TypeAlias = ("""int
TC008.py:61:17: TC008 [*] Remove quotes from type alias
|
60 | # P, Q, and R should not have parenthesis added
61 | p: TypeAlias = ("""int
| _________________^
62 | | | None""")
| |_________^ TC008
63 | type P = ("""int
64 | | None""")
|
= help: Remove quotes
Safe fix
58 58 | | None"""
59 59 |
60 60 | # P, Q, and R should not have parenthesis added
61 |-p: TypeAlias = ("""int
62 |-| None""")
61 |+p: TypeAlias = (int
62 |+| None)
63 63 | type P = ("""int
64 64 | | None""")
65 65 |
TC008.py:63:11: TC008 [*] Remove quotes from type alias
|
61 | p: TypeAlias = ("""int
62 | | None""")
63 | type P = ("""int
| ___________^
64 | | | None""")
| |_________^ TC008
65 |
66 | q: TypeAlias = """(int
|
= help: Remove quotes
Safe fix
60 60 | # P, Q, and R should not have parenthesis added
61 61 | p: TypeAlias = ("""int
62 62 | | None""")
63 |-type P = ("""int
64 |-| None""")
63 |+type P = (int
64 |+| None)
65 65 |
66 66 | q: TypeAlias = """(int
67 67 | | None)"""
TC008.py:66:16: TC008 [*] Remove quotes from type alias
|
64 | | None""")
65 |
66 | q: TypeAlias = """(int
| ________________^
67 | | | None)"""
| |__________^ TC008
68 | type Q = """(int
69 | | None)"""
|
= help: Remove quotes
Safe fix
63 63 | type P = ("""int
64 64 | | None""")
65 65 |
66 |-q: TypeAlias = """(int
67 |-| None)"""
66 |+q: TypeAlias = (int
67 |+| None)
68 68 | type Q = """(int
69 69 | | None)"""
70 70 |
TC008.py:68:10: TC008 [*] Remove quotes from type alias
|
66 | q: TypeAlias = """(int
67 | | None)"""
68 | type Q = """(int
| __________^
69 | | | None)"""
| |__________^ TC008
70 |
71 | r: TypeAlias = """int | None"""
|
= help: Remove quotes
Safe fix
65 65 |
66 66 | q: TypeAlias = """(int
67 67 | | None)"""
68 |-type Q = """(int
69 |-| None)"""
68 |+type Q = (int
69 |+| None)
70 70 |
71 71 | r: TypeAlias = """int | None"""
72 72 | type R = """int | None"""
TC008.py:71:16: TC008 [*] Remove quotes from type alias
|
69 | | None)"""
70 |
71 | r: TypeAlias = """int | None"""
| ^^^^^^^^^^^^^^^^ TC008
72 | type R = """int | None"""
|
= help: Remove quotes
Safe fix
68 68 | type Q = """(int
69 69 | | None)"""
70 70 |
71 |-r: TypeAlias = """int | None"""
71 |+r: TypeAlias = int | None
72 72 | type R = """int | None"""
TC008.py:72:10: TC008 [*] Remove quotes from type alias
|
71 | r: TypeAlias = """int | None"""
72 | type R = """int | None"""
| ^^^^^^^^^^^^^^^^ TC008
|
= help: Remove quotes
Safe fix
69 69 | | None)"""
70 70 |
71 71 | r: TypeAlias = """int | None"""
72 |-type R = """int | None"""
72 |+type R = int | None

View File

@@ -1,72 +0,0 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
.resolve_qualified_name(&expr_call.func)
.is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"]))
})
}
pub(crate) fn check_os_path_get_calls(
checker: &Checker,
call: &ExprCall,
fn_name: &str,
attr: &str,
fix_enabled: bool,
violation: impl Violation,
) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", fn_name])
{
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
if fix_enabled {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().{attr}")
} else {
format!("{binding}({arg_code}).stat().{attr}")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}
}

View File

@@ -1,5 +1,4 @@
//! Rules from [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/).
mod helpers;
pub(crate) mod rules;
pub(crate) mod violations;
@@ -82,9 +81,6 @@ mod tests {
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -19,20 +19,12 @@ use ruff_text_size::Ranged;
/// ## Example
///
/// ```python
/// from pathlib import Path
///
/// path = Path()
///
/// path.with_suffix("py")
/// ```
///
/// Use instead:
///
/// ```python
/// from pathlib import Path
///
/// path = Path()
///
/// path.with_suffix(".py")
/// ```
///

View File

@@ -1,9 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.path.getatime`.
@@ -35,9 +32,6 @@ use ruff_python_ast::ExprCall;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
@@ -49,25 +43,8 @@ use ruff_python_ast::ExprCall;
pub(crate) struct OsPathGetatime;
impl Violation for OsPathGetatime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getatime` should be replaced by `Path.stat().st_atime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_atime`".to_string())
}
}
/// PTH203
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getatime",
"st_atime",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
);
}

View File

@@ -1,9 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.path.getctime`.
@@ -35,9 +32,6 @@ use ruff_python_ast::ExprCall;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
@@ -49,26 +43,8 @@ use ruff_python_ast::ExprCall;
pub(crate) struct OsPathGetctime;
impl Violation for OsPathGetctime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getctime` should be replaced by `Path.stat().st_ctime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_ctime`".to_string())
}
}
/// PTH205
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getctime",
"st_ctime",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
);
}

View File

@@ -1,9 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.path.getmtime`.
@@ -35,9 +32,6 @@ use ruff_python_ast::ExprCall;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
@@ -49,26 +43,8 @@ use ruff_python_ast::ExprCall;
pub(crate) struct OsPathGetmtime;
impl Violation for OsPathGetmtime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getmtime` should be replaced by `Path.stat().st_mtime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_mtime`".to_string())
}
}
/// PTH204
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getmtime",
"st_mtime",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
);
}

View File

@@ -1,9 +1,11 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
/// ## What it does
/// Checks for uses of `os.path.getsize`.
@@ -63,12 +65,63 @@ impl Violation for OsPathGetsize {
/// PTH202
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getsize",
"st_size",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
);
if !matches!(
checker
.semantic()
.resolve_qualified_name(&call.func)
.as_ref()
.map(QualifiedName::segments),
Some(["os", "path", "getsize"])
) {
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let mut diagnostic = checker.report_diagnostic(OsPathGetsize, range);
if is_fix_os_path_getsize_enabled(checker.settings()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().st_size")
} else {
format!("{binding}({arg_code}).stat().st_size")
};
Ok(
Fix::safe_edits(Edit::range_replacement(replacement, range), [import_edit])
.with_applicability(applicability),
)
});
}
}
fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
.resolve_qualified_name(&expr_call.func)
.is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"]))
})
}

View File

@@ -4,7 +4,9 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::rules::Glob;
use crate::rules::flake8_use_pathlib::rules::{
Glob, OsPathGetatime, OsPathGetctime, OsPathGetmtime,
};
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath,
OsPathBasename, OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir,
@@ -192,6 +194,12 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH203
["os", "path", "getatime"] => checker.report_diagnostic_if_enabled(OsPathGetatime, range),
// PTH204
["os", "path", "getmtime"] => checker.report_diagnostic_if_enabled(OsPathGetmtime, range),
// PTH205
["os", "path", "getctime"] => checker.report_diagnostic_if_enabled(OsPathGetctime, range),
// PTH211
["os", "symlink"] => {
// `dir_fd` is not supported by pathlib, so check if there are non-default values.

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
PTH202.py:10:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
@@ -14,7 +14,7 @@ PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
@@ -25,7 +25,7 @@ PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -35,7 +35,7 @@ PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
@@ -46,7 +46,7 @@ PTH202.py:15:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
@@ -56,7 +56,7 @@ PTH202.py:16:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -66,7 +66,7 @@ PTH202.py:17:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
@@ -77,7 +77,7 @@ PTH202.py:19:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
@@ -87,7 +87,7 @@ PTH202.py:20:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
@@ -98,7 +98,7 @@ PTH202.py:21:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -108,7 +108,7 @@ PTH202.py:22:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
@@ -119,7 +119,7 @@ PTH202.py:24:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
@@ -129,7 +129,7 @@ PTH202.py:25:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
@@ -140,7 +140,7 @@ PTH202.py:26:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -150,7 +150,7 @@ PTH202.py:27:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
@@ -161,7 +161,7 @@ PTH202.py:29:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
@@ -171,7 +171,7 @@ PTH202.py:30:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
@@ -182,7 +182,7 @@ PTH202.py:31:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -192,7 +192,7 @@ PTH202.py:32:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
@@ -203,7 +203,7 @@ PTH202.py:34:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
@@ -213,7 +213,7 @@ PTH202.py:35:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -223,70 +223,89 @@ PTH202.py:36:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:39:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
40 | "filename", # comment
41 | )
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:43:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
41 | )
42 |
43 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
44 | # comment
45 | "filename"
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:50:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
48 | )
49 |
50 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
51 | # comment
52 | b"filename"
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:56:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
54 | )
55 |
56 | os.path.getsize( # comment
| ^^^^^^^^^^^^^^^ PTH202
57 | Path(__file__)
58 | # comment
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:61:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
59 | ) # comment
60 |
61 | getsize( # comment
| ^^^^^^^ PTH202
62 | "filename")
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:64:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
62 | "filename")
63 |
64 | getsize( # comment
| ^^^^^^^ PTH202
65 | b"filename",
66 | #comment
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
|
= help: Replace with `Path(...).stat().st_size`
@@ -295,7 +314,7 @@ PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
@@ -303,12 +322,17 @@ PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
PTH202.py:71:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
69 | os.path.getsize("file" + "name")
70 |
71 | getsize \
| ^^^^^^^ PTH202
72 | \
73 | \
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
|
= help: Replace with `Path(...).stat().st_size`
@@ -317,7 +341,7 @@ PTH202.py:78:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
79 |
80 | import pathlib
|
@@ -328,6 +352,6 @@ PTH202.py:82:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -6,7 +6,7 @@ PTH202_2.py:3:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
@@ -16,7 +16,7 @@ PTH202_2.py:4:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -26,6 +26,6 @@ PTH202_2.py:5:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -10,7 +10,6 @@ PTH203.py:5:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -19,7 +18,6 @@ PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
| ^^^^^^^^^^^^^^^^ PTH203
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -28,7 +26,6 @@ PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
7 | os.path.getatime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -37,7 +34,6 @@ PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
11 | getatime(b"filename")
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -46,7 +42,6 @@ PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH203
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -55,88 +50,3 @@ PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
12 | getatime(Path("filename"))
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:17:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
15 | file = __file__
16 |
17 | os.path.getatime(file)
| ^^^^^^^^^^^^^^^^ PTH203
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:18:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
| ^^^^^^^^^^^^^^^^ PTH203
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:19:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
20 |
21 | os.path.getatime( # comment 1
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:21:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
19 | os.path.getatime(filename=Path("filename"))
20 |
21 | os.path.getatime( # comment 1
| ^^^^^^^^^^^^^^^^ PTH203
22 | # comment 2
23 | "filename" # comment 3
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:29:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
27 | ) # comment 7
28 |
29 | os.path.getatime("file" + "name")
| ^^^^^^^^^^^^^^^^ PTH203
30 |
31 | getatime(Path("filename").resolve())
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:31:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
29 | os.path.getatime("file" + "name")
30 |
31 | getatime(Path("filename").resolve())
| ^^^^^^^^ PTH203
32 |
33 | os.path.getatime(pathlib.Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:33:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
31 | getatime(Path("filename").resolve())
32 |
33 | os.path.getatime(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
34 |
35 | getatime(Path("dir") / "file.txt")
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:35:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
33 | os.path.getatime(pathlib.Path("filename"))
34 |
35 | getatime(Path("dir") / "file.txt")
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
snapshot_kind: text
---
PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -8,7 +9,6 @@ PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -17,7 +17,6 @@ PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
| ^^^^^^^^^^^^^^^^ PTH204
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -26,7 +25,6 @@ PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
8 | os.path.getmtime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -35,7 +33,6 @@ PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -44,7 +41,6 @@ PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH204
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -53,4 +49,3 @@ PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
13 | getmtime(Path("filename"))
| ^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`

View File

@@ -8,7 +8,6 @@ PTH205.py:6:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -17,7 +16,6 @@ PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
| ^^^^^^^^^^^^^^^^ PTH205
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -28,7 +26,6 @@ PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
9 |
10 | getctime("filename")
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -39,7 +36,6 @@ PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
11 | getctime(b"filename")
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -48,7 +44,6 @@ PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH205
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -57,4 +52,3 @@ PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
12 | getctime(Path("filename"))
| ^^^^^^^^ PTH205
|
= help: Replace with `Path.stat(...).st_ctime`

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
PTH202.py:10:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
@@ -24,7 +24,7 @@ PTH202.py:11:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
@@ -45,7 +45,7 @@ PTH202.py:12:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -65,7 +65,7 @@ PTH202.py:13:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
@@ -86,7 +86,7 @@ PTH202.py:15:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
@@ -106,7 +106,7 @@ PTH202.py:16:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -126,7 +126,7 @@ PTH202.py:17:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
@@ -147,7 +147,7 @@ PTH202.py:19:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
@@ -167,7 +167,7 @@ PTH202.py:20:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
@@ -188,7 +188,7 @@ PTH202.py:21:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -208,7 +208,7 @@ PTH202.py:22:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
@@ -229,7 +229,7 @@ PTH202.py:24:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
@@ -249,7 +249,7 @@ PTH202.py:25:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
@@ -270,7 +270,7 @@ PTH202.py:26:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -290,7 +290,7 @@ PTH202.py:27:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
@@ -311,7 +311,7 @@ PTH202.py:29:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
@@ -331,7 +331,7 @@ PTH202.py:30:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
@@ -352,7 +352,7 @@ PTH202.py:31:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -372,7 +372,7 @@ PTH202.py:32:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
@@ -393,7 +393,7 @@ PTH202.py:34:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
@@ -413,7 +413,7 @@ PTH202.py:35:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -433,7 +433,7 @@ PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
@@ -449,10 +449,12 @@ PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
40 | "filename", # comment
41 | )
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
@@ -470,12 +472,17 @@ PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
41 | )
42 |
43 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
44 | # comment
45 | "filename"
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
@@ -496,12 +503,16 @@ PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
48 | )
49 |
50 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
51 | # comment
52 | b"filename"
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -521,12 +532,15 @@ PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
54 | )
55 |
56 | os.path.getsize( # comment
| ^^^^^^^^^^^^^^^ PTH202
57 | Path(__file__)
58 | # comment
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -545,11 +559,13 @@ PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
59 | ) # comment
60 |
61 | getsize( # comment
| ^^^^^^^ PTH202
62 | "filename")
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -566,12 +582,15 @@ PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:64:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
62 | "filename")
63 |
64 | getsize( # comment
| ^^^^^^^ PTH202
65 | b"filename",
66 | #comment
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
|
= help: Replace with `Path(...).stat().st_size`
@@ -593,7 +612,7 @@ PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
@@ -611,12 +630,17 @@ PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:71:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
69 | os.path.getsize("file" + "name")
70 |
71 | getsize \
| ^^^^^^^ PTH202
72 | \
73 | \
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
|
= help: Replace with `Path(...).stat().st_size`
@@ -640,7 +664,7 @@ PTH202.py:78:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
79 |
80 | import pathlib
|
@@ -661,7 +685,7 @@ PTH202.py:82:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -6,7 +6,7 @@ PTH202_2.py:3:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
@@ -25,7 +25,7 @@ PTH202_2.py:4:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -44,7 +44,7 @@ PTH202_2.py:5:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -1,284 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH203.py:5:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
3 | from os.path import getatime
4 |
5 | os.path.getatime("filename")
| ^^^^^^^^^^^^^^^^ PTH203
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
2 2 | from pathlib import Path
3 3 | from os.path import getatime
4 4 |
5 |-os.path.getatime("filename")
5 |+Path("filename").stat().st_atime
6 6 | os.path.getatime(b"filename")
7 7 | os.path.getatime(Path("filename"))
8 8 |
PTH203.py:6:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH203
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
3 3 | from os.path import getatime
4 4 |
5 5 | os.path.getatime("filename")
6 |-os.path.getatime(b"filename")
6 |+Path(b"filename").stat().st_atime
7 7 | os.path.getatime(Path("filename"))
8 8 |
9 9 |
PTH203.py:7:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
4 4 |
5 5 | os.path.getatime("filename")
6 6 | os.path.getatime(b"filename")
7 |-os.path.getatime(Path("filename"))
7 |+Path("filename").stat().st_atime
8 8 |
9 9 |
10 10 | getatime("filename")
PTH203.py:10:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
| ^^^^^^^^ PTH203
11 | getatime(b"filename")
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
7 7 | os.path.getatime(Path("filename"))
8 8 |
9 9 |
10 |-getatime("filename")
10 |+Path("filename").stat().st_atime
11 11 | getatime(b"filename")
12 12 | getatime(Path("filename"))
13 13 |
PTH203.py:11:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
| ^^^^^^^^ PTH203
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
8 8 |
9 9 |
10 10 | getatime("filename")
11 |-getatime(b"filename")
11 |+Path(b"filename").stat().st_atime
12 12 | getatime(Path("filename"))
13 13 |
14 14 |
PTH203.py:12:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
12 | getatime(Path("filename"))
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
9 9 |
10 10 | getatime("filename")
11 11 | getatime(b"filename")
12 |-getatime(Path("filename"))
12 |+Path("filename").stat().st_atime
13 13 |
14 14 |
15 15 | file = __file__
PTH203.py:17:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
15 | file = __file__
16 |
17 | os.path.getatime(file)
| ^^^^^^^^^^^^^^^^ PTH203
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
14 14 |
15 15 | file = __file__
16 16 |
17 |-os.path.getatime(file)
17 |+Path(file).stat().st_atime
18 18 | os.path.getatime(filename="filename")
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
PTH203.py:18:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
| ^^^^^^^^^^^^^^^^ PTH203
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
15 15 | file = __file__
16 16 |
17 17 | os.path.getatime(file)
18 |-os.path.getatime(filename="filename")
18 |+Path("filename").stat().st_atime
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
21 21 | os.path.getatime( # comment 1
PTH203.py:19:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
20 |
21 | os.path.getatime( # comment 1
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
16 16 |
17 17 | os.path.getatime(file)
18 18 | os.path.getatime(filename="filename")
19 |-os.path.getatime(filename=Path("filename"))
19 |+Path("filename").stat().st_atime
20 20 |
21 21 | os.path.getatime( # comment 1
22 22 | # comment 2
PTH203.py:21:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
19 | os.path.getatime(filename=Path("filename"))
20 |
21 | os.path.getatime( # comment 1
| ^^^^^^^^^^^^^^^^ PTH203
22 | # comment 2
23 | "filename" # comment 3
|
= help: Replace with `Path.stat(...).st_atime`
Unsafe fix
18 18 | os.path.getatime(filename="filename")
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
21 |-os.path.getatime( # comment 1
22 |- # comment 2
23 |- "filename" # comment 3
24 |- # comment 4
25 |- , # comment 5
26 |- # comment 6
27 |-) # comment 7
21 |+Path("filename").stat().st_atime # comment 7
28 22 |
29 23 | os.path.getatime("file" + "name")
30 24 |
PTH203.py:29:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
27 | ) # comment 7
28 |
29 | os.path.getatime("file" + "name")
| ^^^^^^^^^^^^^^^^ PTH203
30 |
31 | getatime(Path("filename").resolve())
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
26 26 | # comment 6
27 27 | ) # comment 7
28 28 |
29 |-os.path.getatime("file" + "name")
29 |+Path("file" + "name").stat().st_atime
30 30 |
31 31 | getatime(Path("filename").resolve())
32 32 |
PTH203.py:31:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
29 | os.path.getatime("file" + "name")
30 |
31 | getatime(Path("filename").resolve())
| ^^^^^^^^ PTH203
32 |
33 | os.path.getatime(pathlib.Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
28 28 |
29 29 | os.path.getatime("file" + "name")
30 30 |
31 |-getatime(Path("filename").resolve())
31 |+Path(Path("filename").resolve()).stat().st_atime
32 32 |
33 33 | os.path.getatime(pathlib.Path("filename"))
34 34 |
PTH203.py:33:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
31 | getatime(Path("filename").resolve())
32 |
33 | os.path.getatime(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
34 |
35 | getatime(Path("dir") / "file.txt")
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
30 30 |
31 31 | getatime(Path("filename").resolve())
32 32 |
33 |-os.path.getatime(pathlib.Path("filename"))
33 |+pathlib.Path("filename").stat().st_atime
34 34 |
35 35 | getatime(Path("dir") / "file.txt")
PTH203.py:35:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
33 | os.path.getatime(pathlib.Path("filename"))
34 |
35 | getatime(Path("dir") / "file.txt")
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
32 32 |
33 33 | os.path.getatime(pathlib.Path("filename"))
34 34 |
35 |-getatime(Path("dir") / "file.txt")
35 |+Path(Path("dir") / "file.txt").stat().st_atime

View File

@@ -1,110 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH204.py:6:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
| ^^^^^^^^^^^^^^^^ PTH204
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
3 3 | from os.path import getmtime
4 4 |
5 5 |
6 |-os.path.getmtime("filename")
6 |+Path("filename").stat().st_mtime
7 7 | os.path.getmtime(b"filename")
8 8 | os.path.getmtime(Path("filename"))
9 9 |
PTH204.py:7:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
7 | os.path.getmtime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH204
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
4 4 |
5 5 |
6 6 | os.path.getmtime("filename")
7 |-os.path.getmtime(b"filename")
7 |+Path(b"filename").stat().st_mtime
8 8 | os.path.getmtime(Path("filename"))
9 9 |
10 10 |
PTH204.py:8:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
5 5 |
6 6 | os.path.getmtime("filename")
7 7 | os.path.getmtime(b"filename")
8 |-os.path.getmtime(Path("filename"))
8 |+Path("filename").stat().st_mtime
9 9 |
10 10 |
11 11 | getmtime("filename")
PTH204.py:11:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
| ^^^^^^^^ PTH204
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
8 8 | os.path.getmtime(Path("filename"))
9 9 |
10 10 |
11 |-getmtime("filename")
11 |+Path("filename").stat().st_mtime
12 12 | getmtime(b"filename")
13 13 | getmtime(Path("filename"))
PTH204.py:12:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
12 | getmtime(b"filename")
| ^^^^^^^^ PTH204
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
9 9 |
10 10 |
11 11 | getmtime("filename")
12 |-getmtime(b"filename")
12 |+Path(b"filename").stat().st_mtime
13 13 | getmtime(Path("filename"))
PTH204.py:13:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
| ^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
10 10 |
11 11 | getmtime("filename")
12 12 | getmtime(b"filename")
13 |-getmtime(Path("filename"))
13 |+Path("filename").stat().st_mtime

View File

@@ -1,114 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH205.py:6:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
| ^^^^^^^^^^^^^^^^ PTH205
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
3 3 | from os.path import getctime
4 4 |
5 5 |
6 |-os.path.getctime("filename")
6 |+Path("filename").stat().st_ctime
7 7 | os.path.getctime(b"filename")
8 8 | os.path.getctime(Path("filename"))
9 9 |
PTH205.py:7:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
7 | os.path.getctime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH205
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
4 4 |
5 5 |
6 6 | os.path.getctime("filename")
7 |-os.path.getctime(b"filename")
7 |+Path(b"filename").stat().st_ctime
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 10 | getctime("filename")
PTH205.py:8:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH205
9 |
10 | getctime("filename")
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
5 5 |
6 6 | os.path.getctime("filename")
7 7 | os.path.getctime(b"filename")
8 |-os.path.getctime(Path("filename"))
8 |+Path("filename").stat().st_ctime
9 9 |
10 10 | getctime("filename")
11 11 | getctime(b"filename")
PTH205.py:10:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
8 | os.path.getctime(Path("filename"))
9 |
10 | getctime("filename")
| ^^^^^^^^ PTH205
11 | getctime(b"filename")
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
7 7 | os.path.getctime(b"filename")
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 |-getctime("filename")
10 |+Path("filename").stat().st_ctime
11 11 | getctime(b"filename")
12 12 | getctime(Path("filename"))
PTH205.py:11:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
10 | getctime("filename")
11 | getctime(b"filename")
| ^^^^^^^^ PTH205
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 10 | getctime("filename")
11 |-getctime(b"filename")
11 |+Path(b"filename").stat().st_ctime
12 12 | getctime(Path("filename"))
PTH205.py:12:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
10 | getctime("filename")
11 | getctime(b"filename")
12 | getctime(Path("filename"))
| ^^^^^^^^ PTH205
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
9 9 |
10 10 | getctime("filename")
11 11 | getctime(b"filename")
12 |-getctime(Path("filename"))
12 |+Path("filename").stat().st_ctime

View File

@@ -24,14 +24,13 @@ use crate::checkers::ast::Checker;
/// ## Example
/// ```python
/// with open("file", "rwx") as f:
/// content = f.read()
/// return f.read()
/// ```
///
/// Use instead:
///
/// ```python
/// with open("file", "r") as f:
/// content = f.read()
/// return f.read()
/// ```
///
/// ## References

View File

@@ -15,23 +15,21 @@ use crate::{Edit, Fix, FixAvailability, Violation};
///
/// ## Example
/// ```python
/// def bar():
/// for x in foo:
/// yield x
/// for x in foo:
/// yield x
///
/// global y
/// for y in foo:
/// yield y
/// global y
/// for y in foo:
/// yield y
/// ```
///
/// Use instead:
/// ```python
/// def bar():
/// yield from foo
/// yield from foo
///
/// for _element in foo:
/// y = _element
/// yield y
/// for _element in foo:
/// y = _element
/// yield y
/// ```
///
/// ## Fix safety

View File

@@ -91,7 +91,7 @@ impl SequenceType {
.map(Ranged::start)
.unwrap_or(pattern.end()),
)];
let after_last_pattern = &source[TextRange::new(
let after_last_patttern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
@@ -100,7 +100,7 @@ impl SequenceType {
.unwrap_or(pattern.end()),
)];
if before_first_pattern.starts_with('[') && !after_last_pattern.ends_with(',') {
if before_first_pattern.starts_with('[') && !after_last_patttern.ends_with(',') {
SequenceType::List
} else if before_first_pattern.starts_with('(') {
// If the pattern is empty, it must be a parenthesized tuple with no members. (This

View File

@@ -26,11 +26,8 @@ use self::traits::{NotificationHandler, RequestHandler};
use super::{Result, schedule::BackgroundSchedule};
/// Defines the `document_url` method for implementers of [`Notification`] and [`Request`], given
/// the request or notification parameter type.
///
/// This would only work if the parameter type has a `text_document` field with a `uri` field
/// that is of type [`lsp_types::Url`].
/// Defines the `document_url` method for implementers of [`traits::Notification`] and [`traits::Request`],
/// given the parameter type used by the implementer.
macro_rules! define_document_url {
($params:ident: &$p:ty) => {
fn document_url($params: &$p) -> std::borrow::Cow<lsp_types::Url> {

View File

@@ -1,31 +1,4 @@
//! Traits for handling requests and notifications from the LSP client.
//!
//! This module defines the trait abstractions used by the language server to handle incoming
//! requests and notifications from clients. It provides a type-safe way to implement LSP handlers
//! with different execution models (synchronous or asynchronous) and automatic retry capabilities.
//!
//! All request and notification handlers must implement the base traits [`RequestHandler`] and
//! [`NotificationHandler`], respectively, which associate them with specific LSP request or
//! notification types. These base traits are then extended by more specific traits that define
//! the execution model of the handler.
//!
//! The [`SyncRequestHandler`] and [`SyncNotificationHandler`] traits are for handlers that
//! executes synchronously on the main loop, providing mutable access to the [`Session`] that
//! contains the current state of the server. This is useful for handlers that need to modify
//! the server state such as when the content of a file changes.
//!
//! The [`BackgroundDocumentRequestHandler`] and [`BackgroundDocumentNotificationHandler`] traits
//! are for handlers that operate on a single document and can be executed on a background thread.
//! These handlers will have access to a snapshot of the document at the time of the request or
//! notification, allowing them to perform operations without blocking the main loop.
//!
//! The [`SyncNotificationHandler`] is the most common trait that would be used because most
//! notifications are specific to a single document and require updating the server state.
//! Similarly, the [`BackgroundDocumentRequestHandler`] is the most common request handler that
//! would be used as most requests are document-specific and can be executed in the background.
//!
//! See the `./requests` and `./notifications` directories for concrete implementations of these
//! traits in action.
//! A stateful LSP implementation that calls into the Ruff API.
use crate::session::{Client, DocumentSnapshot, Session};
@@ -39,10 +12,9 @@ pub(super) trait RequestHandler {
}
/// A request handler that needs mutable access to the session.
///
/// This will block the main message receiver loop, meaning that no incoming requests or
/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or
/// long-running computations.
/// This will block the main message receiver loop, meaning that no
/// incoming requests or notifications will be handled while `run` is
/// executing. Try to avoid doing any I/O or long-running computations.
pub(super) trait SyncRequestHandler: RequestHandler {
fn run(
session: &mut Session,
@@ -52,14 +24,10 @@ pub(super) trait SyncRequestHandler: RequestHandler {
}
/// A request handler that can be run on a background thread.
///
/// This handler is specific to requests that operate on a single document.
pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
/// Returns the URL of the document that this request handler operates on.
///
/// This method can be implemented automatically using the [`define_document_url`] macro.
///
/// [`define_document_url`]: super::define_document_url
/// `document_url` can be implemented automatically with
/// `define_document_url!(params: &<YourParameterType>)` in the trait
/// implementation.
fn document_url(
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
) -> std::borrow::Cow<lsp_types::Url>;
@@ -79,10 +47,9 @@ pub(super) trait NotificationHandler {
}
/// A notification handler that needs mutable access to the session.
///
/// This will block the main message receiver loop, meaning that no incoming requests or
/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or
/// long-running computations.
/// This will block the main message receiver loop, meaning that no
/// incoming requests or notifications will be handled while `run` is
/// executing. Try to avoid doing any I/O or long-running computations.
pub(super) trait SyncNotificationHandler: NotificationHandler {
fn run(
session: &mut Session,
@@ -93,11 +60,9 @@ pub(super) trait SyncNotificationHandler: NotificationHandler {
/// A notification handler that can be run on a background thread.
pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler {
/// Returns the URL of the document that this notification handler operates on.
///
/// This method can be implemented automatically using the [`define_document_url`] macro.
///
/// [`define_document_url`]: super::define_document_url
/// `document_url` can be implemented automatically with
/// `define_document_url!(params: &<YourParameterType>)` in the trait
/// implementation.
fn document_url(
params: &<<Self as NotificationHandler>::NotificationType as LSPNotification>::Params,
) -> std::borrow::Cow<lsp_types::Url>;

View File

@@ -297,7 +297,6 @@ possibly-unresolved-reference = "ignore"
A list of file and directory patterns to exclude from type checking.
Patterns follow a syntax similar to `.gitignore`:
- `./src/` matches only a directory
- `./src` matches both files and directories
- `src` matches files or directories named `src`

View File

@@ -296,11 +296,6 @@ impl MainLoop {
tracing::warn!("No python files found under the given path(s)");
}
// TODO: We should have an official flag to silence workspace diagnostics.
if std::env::var("TY_MEMORY_REPORT").as_deref() == Ok("mypy_primer") {
return Ok(ExitStatus::Success);
}
let mut stdout = stdout().lock();
if result.is_empty() {

View File

@@ -11,7 +11,6 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
bitflags = { workspace = true }
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }

View File

@@ -1,12 +1,13 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use crate::{Db, HasNavigationTargets, NavigationTarget, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::Type;
use ty_python_semantic::{HasType, SemanticModel};
use ty_python_semantic::{HasDefinition, HasType, SemanticModel};
pub fn goto_type_definition(
db: &dyn Db,
@@ -29,6 +30,35 @@ pub fn goto_type_definition(
})
}
pub fn goto_definition(
db: &dyn Db,
file: File,
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let definitions = goto_target.definitions(&model)?;
tracing::debug!("Definitions of covering node is found");
let targets = definitions.into_iter().map(|definition| {
let full_range = definition.full_range(db, &module);
NavigationTarget {
file: full_range.file(),
focus_range: definition.focus_range(db, &module).range(),
full_range: full_range.range(),
}
});
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: NavigationTargets::unique(targets),
})
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>),
@@ -154,6 +184,16 @@ impl GotoTarget<'_> {
Some(ty)
}
pub(crate) fn definitions<'db>(
self,
model: &SemanticModel<'db>,
) -> Option<Vec<Definition<'db>>> {
match self {
GotoTarget::Expression(expr_ref) => expr_ref.definitions(model),
_ => None,
}
}
}
impl Ranged for GotoTarget<'_> {
@@ -254,7 +294,7 @@ pub(crate) fn find_goto_target(
#[cfg(test)]
mod tests {
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
use crate::{NavigationTarget, goto_type_definition};
use crate::{NavigationTarget, goto_definition, goto_type_definition};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
@@ -792,13 +832,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:691:11
--> stdlib/types.pyi:689:11
|
689 | if sys.version_info >= (3, 10):
690 | @final
691 | class NoneType:
687 | if sys.version_info >= (3, 10):
688 | @final
689 | class NoneType:
| ^^^^^^^^
692 | def __bool__(self) -> Literal[False]: ...
690 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> main.py:3:17
@@ -828,6 +868,516 @@ f(**kwargs<CURSOR>)
");
}
#[test]
fn goto_def_function_call() {
let test = cursor_test(
r#"
def ab(a, b): ...
a<CURSOR>b(1, 2)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:17
|
2 | def ab(a, b): ...
| ^^
3 |
4 | ab(1, 2)
|
info: Source
--> main.py:4:13
|
2 | def ab(a, b): ...
3 |
4 | ab(1, 2)
| ^^
|
");
}
#[test]
fn goto_def_local_load() {
let test = cursor_test(
r#"
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | print(ab)
|
info: Source
--> main.py:3:19
|
2 | ab = 1
3 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_rebind() {
let test = cursor_test(
r#"
ab = 1
ab = 2
ab = 3
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:13
|
2 | ab = 1
3 | ab = 2
4 | ab = 3
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | ab = 2
4 | ab = 3
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_cond_rebind() {
let test = cursor_test(
r#"
ab = 1
if cond:
ab = 2
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | if cond:
4 | ab = 2
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab = 1
3 | if cond:
4 | ab = 2
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_exhaustive_bind() {
let test = cursor_test(
r#"
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:17
|
2 | if cond:
3 | ab = 2
| ^^
4 | else:
5 | ab = 1
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:5:17
|
3 | ab = 2
4 | else:
5 | ab = 1
| ^^
6 | print(ab)
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_only_decl() {
let test = cursor_test(
r#"
ab: int
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_local_load_exhaustive_bind_decl() {
let test = cursor_test(
r#"
ab: int
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab: int
3 | if cond:
4 | ab = 2
| ^^
5 | else:
6 | ab = 1
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:6:17
|
4 | ab = 2
5 | else:
6 | ab = 1
| ^^
7 | print(ab)
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_bind_decl() {
let test = cursor_test(
r#"
ab: int
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:13
|
2 | ab: int
3 | ab = 1
| ^^
4 | print(ab)
|
info: Source
--> main.py:4:19
|
2 | ab: int
3 | ab = 1
4 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_first_store() {
let test = cursor_test(
r#"
a<CURSOR>b = 1
print(ab)
ab = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_second_store() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_loadstore() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b += 2
print(ab)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = A<CURSOR>B(5)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | def __init__(self, val: int):
4 | self.myval = val
|
info: Source
--> main.py:6:17
|
4 | self.myval = val
5 |
6 | x = AB(5)
| ^^
|
");
}
#[test]
fn goto_def_class_implicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_explicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
myval: int
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_path_parent() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
xyz = AB(5)
print(x<CURSOR>yz.myval)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:6:13
|
4 | self.myval = val
5 |
6 | xyz = AB(5)
| ^^^
7 | print(xyz.myval)
|
info: Source
--> main.py:7:19
|
6 | xyz = AB(5)
7 | print(xyz.myval)
| ^^^
|
");
}
#[test]
fn goto_def_class_class_variable() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = AB.RE<CURSOR>D
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_path_parent() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = A<CURSOR>B.RED
"#,
);
assert_snapshot!(test.goto_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | RED = "red"
4 | BLUE = "blue"
|
info: Source
--> main.py:6:17
|
4 | BLUE = "blue"
5 |
6 | x = AB.RED
| ^^
|
"#);
}
#[test]
fn goto_def_global_decl() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_global_load() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_global_store() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
impl CursorTest {
fn goto_type_definition(&self) -> String {
let Some(targets) =
@@ -847,6 +1397,24 @@ f(**kwargs<CURSOR>)
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
fn goto_definition(&self) -> String {
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)
else {
return "No goto target found".to_string();
};
if targets.is_empty() {
return "No definitions found".to_string();
}
let source = targets.range;
self.render_diagnostics(
targets
.into_iter()
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
}
struct GotoTypeDefinitionDiagnostic {

View File

@@ -5,17 +5,13 @@ mod goto;
mod hover;
mod inlay_hints;
mod markup;
mod semantic_tokens;
pub use completion::completion;
pub use db::Db;
pub use goto::goto_type_definition;
pub use goto::{goto_definition, goto_type_definition};
pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind;
pub use semantic_tokens::{
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};

File diff suppressed because it is too large Load Diff

View File

@@ -324,8 +324,8 @@ impl SalsaMemoryDump {
struct DisplayShort<'a>(&'a SalsaMemoryDump);
fn round_memory(total: usize) -> usize {
// Round the number to the nearest power of 1.05. This gives us a
// 2.5% threshold before the memory usage number is considered to have
// Round the number to the nearest power of 1.1. This gives us a
// 5% threshold before the memory usage number is considered to have
// changed.
//
// TODO: Small changes in memory usage may cause the number to be rounded
@@ -334,7 +334,7 @@ impl SalsaMemoryDump {
// over time that are unrelated to the current change. Ideally we could compare
// the exact numbers across runs and compute the difference, but we don't have
// the infrastructure for that currently.
const BASE: f64 = 1.05;
const BASE: f64 = 1.1;
BASE.powf(bytes_to_mb(total).log(BASE).round()) as usize
}

View File

@@ -577,7 +577,6 @@ pub struct SrcOptions {
/// A list of file and directory patterns to exclude from type checking.
///
/// Patterns follow a syntax similar to `.gitignore`:
///
/// - `./src/` matches only a directory
/// - `./src` matches both files and directories
/// - `src` matches files or directories named `src`

View File

@@ -343,7 +343,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: bound method object.__init__() -> None
reveal_type(c.__init__) # revealed: def __init__(self) -> None
reveal_type(c.__class__) # revealed: type
reveal_type(c.__call__) # revealed: (int, /) -> int
```

View File

@@ -37,7 +37,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
# TODO: Should be `bytes` with no error, like mypy and pyright?
# error: [unresolved-attribute]
reveal_type(c_instance.declared_only) # revealed: Unknown
reveal_type(c_instance.declared_and_bound) # revealed: bool
@@ -56,9 +58,6 @@ c_instance.declared_and_bound = "incompatible"
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
reveal_type(C.inferred_from_value) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(C.declared_and_bound) # revealed: Unknown
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
C.inferred_from_value = "overwritten on class"
@@ -144,14 +143,16 @@ class C:
c_instance = C(True)
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
reveal_type(c_instance.only_declared_in_init) # revealed: str | None
# TODO: should be `str | None` without error
# error: [unresolved-attribute]
reveal_type(c_instance.only_declared_in_init) # revealed: Unknown
reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
# which is planned in https://github.com/astral-sh/ruff/issues/14297
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | Literal["a"]
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
```
@@ -182,7 +183,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
# TODO: should be `bytes` with no error, like mypy and pyright?
# error: [unresolved-attribute]
reveal_type(c_instance.declared_only) # revealed: Unknown
reveal_type(c_instance.declared_and_bound) # revealed: bool
@@ -689,14 +692,16 @@ class C:
reveal_type(C.pure_class_variable1) # revealed: str
reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1]
# TODO: Should be `Unknown | Literal[1]`.
reveal_type(C.pure_class_variable2) # revealed: Unknown
c_instance = C()
# It is okay to access a pure class variable on an instance.
reveal_type(c_instance.pure_class_variable1) # revealed: str
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1]
# TODO: Should be `Unknown | Literal[1]`.
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
c_instance.pure_class_variable1 = "value set on instance"
@@ -712,24 +717,6 @@ class Subclass(C):
reveal_type(Subclass.pure_class_variable1) # revealed: str
```
If a class variable is additionally qualified as `Final`, we do not union with `Unknown` for bare
`ClassVar`s:
```py
from typing import Final
class D:
final1: Final[ClassVar] = 1
final2: ClassVar[Final] = 1
final3: ClassVar[Final[int]] = 1
final4: Final[ClassVar[int]] = 1
reveal_type(D.final1) # revealed: Literal[1]
reveal_type(D.final2) # revealed: Literal[1]
reveal_type(D.final3) # revealed: int
reveal_type(D.final4) # revealed: int
```
#### Variable only mentioned in a class method
We also consider a class variable to be a pure class variable if it is only mentioned in a class
@@ -1787,7 +1774,7 @@ date.day = 8
date.month = 4
date.year = 2025
# error: [unresolved-attribute] "Can not assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
# error: [unresolved-attribute] "Can not assign object of `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
date.tz = "UTC"
```

View File

@@ -201,36 +201,6 @@ type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
```
## Method calls on types not disjoint from `None`
Very few methods are defined on `object`, `None`, and other types not disjoint from `None`. However,
descriptor-binding behaviour works on these types in exactly the same way as descriptor binding on
other types. This is despite the fact that `None` is used as a sentinel internally by the descriptor
protocol to indicate that a method was accessed on the class itself rather than an instance of the
class:
```py
from typing import Protocol, Literal
from ty_extensions import AlwaysFalsy
class Foo: ...
class SupportsStr(Protocol):
def __str__(self) -> str: ...
class Falsy(Protocol):
def __bool__(self) -> Literal[False]: ...
def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | None):
a.__str__()
b.__str__()
c.__str__()
d.__str__()
# TODO: these should not error
e.__str__() # error: [missing-argument]
f.__str__() # error: [missing-argument]
```
## Error cases: Calling `__get__` for methods
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
@@ -264,18 +234,16 @@ method_wrapper(C())
method_wrapper(C(), None)
method_wrapper(None, C)
reveal_type(object.__str__.__get__(object(), None)()) # revealed: str
# TODO: passing `None` without an `owner` argument fails at runtime.
# Ideally we would emit a diagnostic here:
# Passing `None` without an `owner` argument is an
# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`"
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, 1)
# TODO: passing `None` as the `owner` argument when `instance` is `None` fails at runtime.
# Ideally we would emit a diagnostic here.
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an

View File

@@ -444,33 +444,6 @@ To do
To do
## `Final` fields
Dataclass fields can be annotated with `Final`, which means that the field cannot be reassigned
after the instance is created. Fields that are additionally annotated with `ClassVar` are not part
of the `__init__` signature.
```py
from dataclasses import dataclass
from typing import Final, ClassVar
@dataclass
class C:
# a `Final` annotation without a right-hand side is not allowed in normal classes,
# but valid for dataclasses. The field will be initialized in the synthesized
# `__init__` method
instance_variable_no_default: Final[int]
instance_variable: Final[int] = 1
class_variable1: ClassVar[Final[int]] = 1
class_variable2: ClassVar[Final[int]] = 1
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
c = C(1)
# TODO: this should be an error
c.instance_variable = 2
```
## Inheritance
### Normal class inheriting from a dataclass

View File

@@ -619,9 +619,8 @@ wrapper_descriptor()
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f)
# TODO: Calling it without the `owner` argument if `instance` is not `None` fails at runtime.
# Ideally we would emit a diagnostic here,
# but this is hard to model without introducing false positives elsewhere
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`"
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):

View File

@@ -2,53 +2,27 @@
## Basic functionality
`assert_never` makes sure that the type of the argument is `Never`.
### Correct usage
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _(never: Never):
assert_never(never) # fine
```
### Diagnostics
<!-- snapshot-diagnostics -->
If it is not, a `type-assertion-failure` diagnostic is emitted.
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
`type-assertion-failure` diagnostic is emitted.
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _():
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
assert_never(never) # fine
assert_never(0) # error: [type-assertion-failure]
def _():
assert_never("") # error: [type-assertion-failure]
def _():
assert_never(None) # error: [type-assertion-failure]
def _():
assert_never([]) # error: [type-assertion-failure]
def _():
assert_never({}) # error: [type-assertion-failure]
def _():
assert_never(()) # error: [type-assertion-failure]
def _(flag: bool, never: Never):
assert_never(1 if flag else never) # error: [type-assertion-failure]
def _(any_: Any):
assert_never(any_) # error: [type-assertion-failure]
def _(unknown: Unknown):
assert_never(unknown) # error: [type-assertion-failure]
```

View File

@@ -205,18 +205,3 @@ python-version = "3.13"
import aifc # error: [unresolved-import]
from distutils import sysconfig # error: [unresolved-import]
```
## Cannot shadow core standard library modules
`types.py`:
```py
x: int
```
```py
# error: [unresolved-import]
from types import x
from types import FunctionType
```

View File

@@ -1476,7 +1476,8 @@ class P1(Protocol):
class P2(Protocol):
def x(self, y: int) -> None: ...
static_assert(is_equivalent_to(P1, P2))
# TODO: this should pass
static_assert(is_equivalent_to(P1, P2)) # error: [static-assert-error]
```
As with protocols that only have non-method members, this also holds true when they appear in
@@ -1486,7 +1487,8 @@ differently ordered unions:
class A: ...
class B: ...
static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
# TODO: this should pass
static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) # error: [static-assert-error]
```
## Narrowing of protocols
@@ -1860,21 +1862,6 @@ class Bar(Protocol):
static_assert(is_equivalent_to(Foo, Bar))
```
### Disjointness of recursive protocol and recursive final type
```py
from typing import Protocol
from ty_extensions import is_disjoint_from, static_assert
class Proto(Protocol):
x: "Proto"
class Nominal:
x: "Nominal"
static_assert(not is_disjoint_from(Proto, Nominal))
```
### Regression test: narrowing with self-referential protocols
This snippet caused us to panic on an early version of the implementation for protocols.
@@ -1894,86 +1881,6 @@ if isinstance(obj, (B, A)):
reveal_type(obj) # revealed: (Unknown & B) | (Unknown & A)
```
### Protocols that use `Self`
`Self` is a `TypeVar` with an upper bound of the class in which it is defined. This means that
`Self` annotations in protocols can also be tricky to handle without infinite recursion and stack
overflows.
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, Self
from ty_extensions import static_assert
class _HashObject(Protocol):
def copy(self) -> Self: ...
class Foo: ...
# Attempting to build this union caused us to overflow on an early version of
# <https://github.com/astral-sh/ruff/pull/18659>
x: Foo | _HashObject
```
Some other similar cases that caused issues in our early `Protocol` implementation:
`a.py`:
```py
from typing_extensions import Protocol, Self
class PGconn(Protocol):
def connect(self) -> Self: ...
class Connection:
pgconn: PGconn
def is_crdb(conn: PGconn) -> bool:
return isinstance(conn, Connection)
```
and:
`b.py`:
```py
from typing_extensions import Protocol
class PGconn(Protocol):
def connect[T: PGconn](self: T) -> T: ...
class Connection:
pgconn: PGconn
def f(x: PGconn):
isinstance(x, Connection)
```
### Recursive protocols used as the first argument to `cast()`
These caused issues in an early version of our `Protocol` implementation due to the fact that we use
a recursive function in our `cast()` implementation to check whether a type contains `Unknown` or
`Todo`. Recklessly recursing into a type causes stack overflows if the type is recursive:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import cast, Protocol
class Iterator[T](Protocol):
def __iter__(self) -> Iterator[T]: ...
def f(value: Iterator):
cast(Iterator, value) # error: [redundant-cast]
```
## TODO
Add tests for:

View File

@@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assert_never.md - `assert_never` - Basic functionality - Diagnostics
mdtest name: assert_never.md - `assert_never` - Basic functionality
mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md
---
@@ -15,47 +15,35 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.
1 | from typing_extensions import assert_never, Never, Any
2 | from ty_extensions import Unknown
3 |
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
4 | def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
5 | assert_never(never) # fine
6 |
7 | def _():
7 | assert_never(0) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
9 |
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
12 |
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
15 |
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
18 |
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
21 |
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
```
# Diagnostics
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:5:5
--> src/mdtest_snippet.py:7:5
|
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
5 | assert_never(never) # fine
6 |
7 | assert_never(0) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-^
| |
| Inferred type of argument is `Literal[0]`
6 |
7 | def _():
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
|
info: `Never` and `Literal[0]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -66,13 +54,13 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:8:5
|
7 | def _():
7 | assert_never(0) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Literal[""]`
9 |
10 | def _():
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
|
info: `Never` and `Literal[""]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -81,15 +69,16 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:11:5
--> src/mdtest_snippet.py:9:5
|
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
7 | assert_never(0) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `None`
12 |
13 | def _():
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
|
info: `Never` and `None` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -98,15 +87,16 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:14:5
--> src/mdtest_snippet.py:10:5
|
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `list[Unknown]`
15 |
16 | def _():
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
|
info: `Never` and `list[Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -115,15 +105,16 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:17:5
--> src/mdtest_snippet.py:11:5
|
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `dict[Unknown, Unknown]`
18 |
19 | def _():
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
|
info: `Never` and `dict[Unknown, Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -132,15 +123,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:20:5
--> src/mdtest_snippet.py:12:5
|
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `tuple[()]`
21 |
22 | def _(flag: bool, never: Never):
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
|
info: `Never` and `tuple[()]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -149,15 +140,16 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:23:5
--> src/mdtest_snippet.py:13:5
|
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--------------------^
| |
| Inferred type of argument is `Literal[1]`
24 |
25 | def _(any_: Any):
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
|
info: `Never` and `Literal[1]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -166,15 +158,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:26:5
--> src/mdtest_snippet.py:15:5
|
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `Any`
27 |
28 | def _(unknown: Unknown):
16 | assert_never(unknown) # error: [type-assertion-failure]
|
info: `Never` and `Any` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -183,10 +175,10 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:29:5
--> src/mdtest_snippet.py:16:5
|
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-------^
| |
| Inferred type of argument is `Unknown`

View File

@@ -570,229 +570,6 @@ def f():
reveal_type(x) # revealed: Literal[1]
```
## Calls to functions returning `Never` / `NoReturn`
These calls should be treated as terminal statements.
### No implicit return
If we see a call to a function returning `Never`, we should be able to understand that the function
cannot implicitly return `None`. In the below examples, verify that there are no errors emitted for
invalid return type.
```py
from typing import NoReturn
import sys
def f() -> NoReturn:
sys.exit(1)
```
Let's try cases where the function annotated with `NoReturn` is some sub-expression.
```py
from typing import NoReturn
import sys
# TODO: this is currently not yet supported
# error: [invalid-return-type]
def _() -> NoReturn:
3 + sys.exit(1)
# TODO: this is currently not yet supported
# error: [invalid-return-type]
def _() -> NoReturn:
3 if sys.exit(1) else 4
```
### Type narrowing
If a variable's type is a union, and some types in the union result in a function marked with
`NoReturn` being called, then we should correctly narrow the variable's type.
```py
from typing import NoReturn
import sys
def g(x: int | None):
if x is None:
sys.exit(1)
# TODO: should be just `int`, not `int | None`
# See https://github.com/astral-sh/ty/issues/685
reveal_type(x) # revealed: int | None
```
### Possibly unresolved diagnostics
If the codepath on which a variable is not defined eventually returns `Never`, use of the variable
should not give any diagnostics.
```py
import sys
def _(flag: bool):
if flag:
x = 3
else:
sys.exit()
x # No possibly-unresolved-references diagnostic here.
```
Similarly, there shouldn't be any diagnostics if the `except` block of a `try/except` construct has
a call with `NoReturn`.
```py
import sys
def _():
try:
x = 3
except:
sys.exit()
x # No possibly-unresolved-references diagnostic here.
```
### Bindings in branches
In case of a `NoReturn` call being present in conditionals, the revealed type of the end of the
branch should reflect the path which did not hit any of the `NoReturn` calls. These tests are
similar to the ones for `return` above.
```py
import sys
def call_in_then_branch(cond: bool):
if cond:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
else:
x = "test"
reveal_type(x) # revealed: Literal["test"]
reveal_type(x) # revealed: Literal["test"]
def call_in_else_branch(cond: bool):
if cond:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
reveal_type(x) # revealed: Literal["test"]
def call_in_both_branches(cond: bool):
if cond:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
sys.exit()
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
sys.exit()
reveal_type(x) # revealed: Never
def call_in_nested_then_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
else:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def call_in_nested_else_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def call_in_both_nested_branches(cond1: bool, cond2: bool):
if cond1:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal0"
if cond2:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
sys.exit()
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
sys.exit()
reveal_type(x) # revealed: Literal["test"]
```
### Overloads
If only some overloads of a function are marked with `NoReturn`, we should run the overload
evaluation algorithm when evaluating the constraints.
```py
from typing import NoReturn, overload
@overload
def f(x: int) -> NoReturn: ...
@overload
def f(x: str) -> int: ...
def f(x): ...
# No errors
def _() -> NoReturn:
f(3)
# This should be an error because of implicitly returning `None`
# error: [invalid-return-type]
def _() -> NoReturn:
f("")
```
### Other callables
If other types of callables are annotated with `NoReturn`, we should still be ablt to infer correct
reachability.
```py
import sys
from typing import NoReturn
class C:
def __call__(self) -> NoReturn:
sys.exit()
def die(self) -> NoReturn:
sys.exit()
# No "implicitly returns `None`" diagnostic
def _() -> NoReturn:
C()()
# No "implicitly returns `None`" diagnostic
def _() -> NoReturn:
C().die()
```
## Nested functions
Free references inside of a function body refer to variables defined in the containing scope.

View File

@@ -300,20 +300,6 @@ static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13]))
static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12]))
```
### Unions containing `Callable`s
Two unions containing different `Callable` types are equivalent even if the unions are differently
ordered:
```py
from ty_extensions import CallableTypeOf, Unknown, is_equivalent_to, static_assert
def f(x): ...
def g(x: Unknown): ...
static_assert(is_equivalent_to(CallableTypeOf[f] | int | str, str | int | CallableTypeOf[g]))
```
### Unions containing `Callable`s containing unions
Differently ordered unions inside `Callable`s inside unions can still be equivalent:

View File

@@ -21,7 +21,8 @@ class C:
reveal_type(C.a) # revealed: int
reveal_type(C.b) # revealed: int
reveal_type(C.c) # revealed: int
reveal_type(C.d) # revealed: Unknown | Literal[1]
# TODO: should be Unknown | Literal[1]
reveal_type(C.d) # revealed: Unknown
reveal_type(C.e) # revealed: int
c = C()

View File

@@ -3,124 +3,44 @@
[`typing.Final`] is a type qualifier that is used to indicate that a symbol may not be reassigned in
any scope. Final names declared in class scopes cannot be overridden in subclasses.
## Basic type inference
### `Final` with type
Declared symbols that are additionally qualified with `Final` use the declared type when accessed
from another scope. Local uses of the symbol will use the inferred type, which may be more specific:
## Basic
`mod.py`:
```py
from typing import Final, Annotated
FINAL_A: Final[int] = 1
FINAL_A: int = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: "Final[int]" = 1
FINAL_F: Final[int]
FINAL_F = 1
FINAL_D: Final = 1
FINAL_E: "Final[int]" = 1
reveal_type(FINAL_A) # revealed: Literal[1]
reveal_type(FINAL_B) # revealed: Literal[1]
reveal_type(FINAL_C) # revealed: Literal[1]
reveal_type(FINAL_D) # revealed: Literal[1]
reveal_type(FINAL_D) # revealed: Literal[1]
reveal_type(FINAL_E) # revealed: Literal[1]
def nonlocal_uses():
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```
Imported types:
```py
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_F
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```
### `Final` without a type
When a symbol is qualified with `Final` but no type is specified, the type is inferred from the
right-hand side of the assignment. We do not union the inferred type with `Unknown`, because the
symbol cannot be modified:
`mod.py`:
```py
from typing import Final
FINAL_A: Final = 1
reveal_type(FINAL_A) # revealed: Literal[1]
def nonlocal_uses():
reveal_type(FINAL_A) # revealed: Literal[1]
```
`main.py`:
```py
from mod import FINAL_A
reveal_type(FINAL_A) # revealed: Literal[1]
```
### In class definitions
```py
from typing import Final
class C:
FINAL_A: Final[int] = 1
FINAL_B: Final = 1
def __init__(self):
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
reveal_type(C.FINAL_A) # revealed: int
reveal_type(C.FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_A) # revealed: int
reveal_type(C().FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_C) # revealed: int
# TODO: this should be `Literal[1]`
reveal_type(C().FINAL_D) # revealed: Unknown
```
## Not modifiable
Symbols qualified with `Final` cannot be reassigned, and attempting to do so will result in an
error:
```py
from typing import Final, Annotated
FINAL_A: Final[int] = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: "Final[int]" = 1
FINAL_E: Final[int]
FINAL_E = 1
FINAL_F: Final = 1
# TODO: all of these should be errors
# TODO: All of these should be errors:
FINAL_A = 2
FINAL_B = 2
FINAL_C = 2
FINAL_D = 2
FINAL_E = 2
FINAL_F = 2
```
Public types:
```py
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E
# TODO: All of these should be Literal[1]
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: Unknown
reveal_type(FINAL_E) # revealed: int
```
## Too many arguments
@@ -162,10 +82,6 @@ from typing import Final
# TODO: This should be an error
NO_RHS: Final
class C:
# TODO: This should be an error
NO_RHS: Final
```
[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final

View File

@@ -1,4 +0,0 @@
flake8
sphinx
prefect
trio

View File

@@ -15,7 +15,7 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
pub use semantic_model::{Completion, HasDefinition, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -156,9 +156,6 @@ pub enum KnownModule {
TyExtensions,
#[strum(serialize = "importlib")]
ImportLib,
#[cfg(test)]
#[strum(serialize = "unittest.mock")]
UnittestMock,
}
impl KnownModule {
@@ -178,8 +175,6 @@ impl KnownModule {
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
Self::TyExtensions => "ty_extensions",
Self::ImportLib => "importlib",
#[cfg(test)]
Self::UnittestMock => "unittest.mock",
}
}

View File

@@ -535,23 +535,14 @@ struct ModuleNameIngredient<'db> {
pub(super) name: ModuleName,
}
/// Returns `true` if the module name refers to a standard library module which can't be shadowed
/// by a first-party module.
///
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the
/// `types` module, which tends to be imported early in Python startup, so can't be consistently
/// shadowed, and is important to type checking.
fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
}
/// Given a module name and a list of search paths in which to lookup modules,
/// attempt to resolve the module name
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
let program = Program::get(db);
let python_version = program.python_version(db);
let resolver_state = ResolverContext::new(db, python_version);
let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str());
let is_builtin_module =
ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str());
let name = RelaxedModuleName::new(name);
let stub_name = name.to_stub_package();
@@ -562,8 +553,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
if is_builtin_module && !search_path.is_standard_library() {
continue;
}

View File

@@ -9,8 +9,8 @@ use crate::semantic_index::{
};
use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map};
use crate::types::{
DynamicType, KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder,
UnionType, binding_type, declaration_type, todo_type,
KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType,
binding_type, declaration_type, todo_type,
};
use crate::{Db, FxOrderSet, KnownModule, Program, resolve_module};
@@ -524,21 +524,6 @@ impl<'db> PlaceAndQualifiers<'db> {
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
}
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
match self {
PlaceAndQualifiers { place, qualifiers }
if (qualifiers.contains(TypeQualifiers::FINAL)
&& place
.ignore_possibly_unbound()
.is_some_and(|ty| ty.is_unknown())) =>
{
Some(*qualifiers)
}
_ => None,
}
}
#[must_use]
pub(crate) fn map_type(
self,
@@ -660,42 +645,6 @@ fn place_by_id<'db>(
ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id),
};
// If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side
// inferred type, without unioning with `Unknown`, because it can not be modified.
if let Some(qualifiers) = declared
.as_ref()
.ok()
.and_then(PlaceAndQualifiers::is_bare_final)
{
let bindings = all_considered_bindings();
return place_from_bindings_impl(db, bindings, requires_explicit_reexport)
.with_qualifiers(qualifiers);
}
// Handle bare `ClassVar` annotations by falling back to the union of `Unknown` and the
// inferred type.
match declared {
Ok(PlaceAndQualifiers {
place: Place::Type(Type::Dynamic(DynamicType::Unknown), declaredness),
qualifiers,
}) if qualifiers.contains(TypeQualifiers::CLASS_VAR) => {
let bindings = all_considered_bindings();
match place_from_bindings_impl(db, bindings, requires_explicit_reexport) {
Place::Type(inferred, boundness) => {
return Place::Type(
UnionType::from_elements(db, [Type::unknown(), inferred]),
boundness,
)
.with_qualifiers(qualifiers);
}
Place::Unbound => {
return Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers);
}
}
}
_ => {}
}
match declared {
// Place is declared, trust the declared type
Ok(

View File

@@ -124,30 +124,6 @@ pub(crate) fn attribute_assignments<'db, 's>(
})
}
/// Returns all attribute declarations (and their method scope IDs) with a symbol name matching
/// the one given for a specific class body scope.
///
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
/// introduces a direct dependency on that file's AST.
pub(crate) fn attribute_declarations<'db, 's>(
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
name: &'s str,
) -> impl Iterator<Item = (DeclarationsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
let place_table = index.place_table(function_scope_id);
let place = place_table.place_id_by_instance_attribute_name(name)?;
let use_def = &index.use_def_maps[function_scope_id];
Some((
use_def.inner.all_reachable_declarations(place),
function_scope_id,
))
})
}
/// Returns all attribute assignments as scope IDs for a specific class body scope.
///
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it

View File

@@ -35,8 +35,8 @@ use crate::semantic_index::place::{
PlaceExprWithFlags, PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId,
};
use crate::semantic_index::predicate::{
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate,
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, PredicateOrLiteral,
ScopedPredicateId, StarImportPlaceholderPredicate,
};
use crate::semantic_index::re_exports::exported_names;
use crate::semantic_index::reachability_constraints::{
@@ -1901,45 +1901,11 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
value,
range: _,
node_index: _,
}) => {
if self.in_module_scope() {
if let Some(expr) = dunder_all_extend_argument(value) {
self.add_standalone_expression(expr);
}
}) if self.in_module_scope() => {
if let Some(expr) = dunder_all_extend_argument(value) {
self.add_standalone_expression(expr);
}
self.visit_expr(value);
// If the statement is a call, it could possibly be a call to a function
// marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special
// kind of constraint to mark the following code as unreachable.
//
// Ideally, these constraints should be added for every call expression, even those in
// sub-expressions and in the module-level scope. But doing so makes the number of
// such constraints so high that it significantly degrades performance. We thus cut
// scope here and add these constraints only at statement level function calls,
// like `sys.exit()`, and not within sub-expression like `3 + sys.exit()` etc.
//
// We also only add these inside function scopes, since considering module-level
// constraints can affect the the type of imported symbols, leading to a lot more
// work in third-party code.
if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
if !self.source_type.is_stub() && self.in_function_scope() {
let callable = self.add_standalone_expression(func);
let call_expr = self.add_standalone_expression(value.as_ref());
let predicate = Predicate {
node: PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
call_expr,
}),
is_positive: false,
};
self.record_reachability_constraint(PredicateOrLiteral::Predicate(
predicate,
));
}
}
}
_ => {
walk_stmt(self, stmt);

View File

@@ -102,16 +102,9 @@ impl PredicateOrLiteral<'_> {
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct CallableAndCallExpr<'db> {
pub(crate) callable: Expression<'db>,
pub(crate) call_expr: Expression<'db>,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum PredicateNode<'db> {
Expression(Expression<'db>),
ReturnsNever(CallableAndCallExpr<'db>),
Pattern(PatternPredicate<'db>),
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
}

View File

@@ -204,8 +204,7 @@ use crate::place::{RequiresExplicitReExport, imported_symbol};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::place_table;
use crate::semantic_index::predicate::{
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
Predicates, ScopedPredicateId,
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
};
use crate::types::{Truthiness, Type, infer_expression_type};
@@ -685,53 +684,6 @@ impl ReachabilityConstraints {
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!predicate.is_positive)
}
PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
call_expr,
}) => {
// We first infer just the type of the callable. In the most likely case that the
// function is not marked with `NoReturn`, or that it always returns `NoReturn`,
// doing so allows us to avoid the more expensive work of inferring the entire call
// expression (which could involve inferring argument types to possibly run the overload
// selection algorithm).
// Avoiding this on the happy-path is important because these constraints can be
// very large in number, since we add them on all statement level function calls.
let ty = infer_expression_type(db, callable);
let overloads_iterator =
if let Some(Type::Callable(callable)) = ty.into_callable(db) {
callable.signatures(db).overloads.iter()
} else {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
};
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
.fold((true, true), |(none, all), overload| {
let overload_returns_never =
overload.return_ty.is_some_and(|return_type| {
return_type.is_equivalent_to(db, Type::Never)
});
(
none && !overload_returns_never,
all && overload_returns_never,
)
});
if no_overloads_return_never {
Truthiness::AlwaysFalse
} else if all_overloads_return_never {
Truthiness::AlwaysTrue
} else {
let call_expr_ty = infer_expression_type(db, call_expr);
if call_expr_ty.is_equivalent_to(db, Type::Never) {
Truthiness::AlwaysTrue
} else {
Truthiness::AlwaysFalse
}
}
.negate_if(!predicate.is_positive)
}
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
PredicateNode::StarImportPlaceholder(star_import) => {
let place_table = place_table(db, star_import.scope(db));

View File

@@ -192,17 +192,6 @@
//! for that place that we need for that use or definition. When we reach the end of the scope, it
//! records the state for each place as the public definitions of that place.
//!
//! ```python
//! x = 1
//! x = 2
//! y = x
//! if flag:
//! x = 3
//! else:
//! x = 4
//! z = x
//! ```
//!
//! Let's walk through the above example. Initially we do not have any record of `x`. When we add
//! the new place (before we process the first binding), we create a new undefined `PlaceState`
//! which has a single live binding (the "unbound" definition) and a single live declaration (the
@@ -509,18 +498,6 @@ impl<'db> UseDefMap<'db> {
.is_always_false()
}
pub(crate) fn is_declaration_reachable(
&self,
db: &dyn crate::Db,
declaration: &DeclarationWithConstraint<'db>,
) -> Truthiness {
self.reachability_constraints.evaluate(
db,
&self.predicates,
declaration.reachability_constraint,
)
}
pub(crate) fn is_binding_reachable(
&self,
db: &dyn crate::Db,

View File

@@ -1,12 +1,14 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::line_index;
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast, ExprContext};
use ruff_python_ast::{Expr, ExprRef, name::Name};
use ruff_source_file::LineIndex;
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, Module, resolve_module};
use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::all_declarations_and_bindings;
@@ -175,6 +177,41 @@ pub struct Completion {
pub builtin: bool,
}
pub trait HasDefinition {
/// Returns the definitions of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>>;
}
impl HasDefinition for ast::ExprRef<'_> {
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>> {
match self {
ExprRef::Name(name) => match name.ctx {
ExprContext::Load => {
let index = semantic_index(model.db, model.file);
let file_scope = index.expression_scope_id(*self);
let scope = file_scope.to_scope_id(model.db, model.file);
let use_def = index.use_def_map(file_scope);
let use_id = self.scoped_use_id(model.db, scope);
Some(
use_def
.bindings_at_use(use_id)
.filter_map(|binding| binding.binding.definition())
.collect(),
)
}
ExprContext::Store => None,
ExprContext::Del => None,
ExprContext::Invalid => None,
},
_ => None,
}
}
}
pub trait HasType {
/// Returns the inferred type of `self`.
///

View File

@@ -19,7 +19,7 @@ use ruff_text_size::{Ranged, TextRange};
use type_ordering::union_or_intersection_elements_ordering;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
pub(crate) use self::cyclic::TypeTransformer;
pub use self::diagnostic::TypeCheckDiagnostics;
pub(crate) use self::diagnostic::register_lints;
pub(crate) use self::infer::{
@@ -46,7 +46,7 @@ use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
walk_partial_specialization, walk_specialization,
};
pub use crate::types::ide_support::{all_members, definition_kind_for_name};
pub use crate::types::ide_support::all_members;
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
@@ -1095,74 +1095,6 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn into_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Type::Callable(_) => Some(self),
Type::Dynamic(_) => Some(CallableType::single(db, Signature::dynamic(self))),
Type::FunctionLiteral(function_literal) => {
Some(Type::Callable(function_literal.into_callable_type(db)))
}
Type::BoundMethod(bound_method) => Some(bound_method.into_callable_type(db)),
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
if let Place::Type(ty, Boundness::Bound) = call_symbol {
ty.into_callable(db)
} else {
None
}
}
Type::ClassLiteral(class_literal) => {
Some(ClassType::NonGeneric(class_literal).into_callable(db))
}
Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)),
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(class) => Some(class.into_callable(db)),
SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))),
)),
},
Type::Union(union) => union.try_map(db, |element| element.into_callable(db)),
Type::Never
| Type::DataclassTransformer(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::Tuple(_)
| Type::TypeIs(_) => None,
// TODO
Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::ModuleLiteral(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
| Type::Intersection(_)
| Type::TypeVar(_)
| Type::BoundSuper(_) => None,
}
}
/// Return true if this type is a [subtype of] type `target`.
///
/// For fully static types, this means that the set of objects represented by `self` is a
@@ -1373,14 +1305,24 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_),
) => false,
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
self_callable.has_relation_to(db, other_callable, relation)
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
// If the type of __call__ is a subtype of a callable type, this instance is.
// Don't add other special cases here; our subtyping of a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Place::Type(t, Boundness::Bound) = call_symbol {
t.has_relation_to(db, target, relation)
} else {
false
}
}
(_, Type::Callable(_)) => self
.into_callable(db)
.is_some_and(|callable| callable.has_relation_to(db, target, relation)),
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.has_relation_to(db, right, relation)
}
@@ -1407,6 +1349,16 @@ impl<'db> Type<'db> {
) => (self.literal_fallback_instance(db))
.is_some_and(|instance| instance.has_relation_to(db, target, relation)),
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)
.has_relation_to(db, target, relation)
}
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
.into_callable_type(db)
.has_relation_to(db, target, relation),
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
// so it also, for now, just delegates to its instance fallback.
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
@@ -1424,6 +1376,10 @@ impl<'db> Type<'db> {
.to_instance(db)
.has_relation_to(db, target, relation),
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
self_callable.has_relation_to(db, other_callable, relation)
}
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
// TODO: Implement subtyping using an equivalent `Callable` type.
false
@@ -1500,6 +1456,26 @@ impl<'db> Type<'db> {
self_subclass_ty.has_relation_to(db, target_subclass_ty, relation)
}
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
ClassType::NonGeneric(class_literal)
.into_callable(db)
.has_relation_to(db, target, relation)
}
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
.into_callable(db)
.has_relation_to(db, target, relation),
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
(Type::SubclassOf(subclass_of_ty), Type::Callable(_))
if subclass_of_ty.subclass_of().into_class().is_some() =>
{
let class = subclass_of_ty.subclass_of().into_class().unwrap();
class
.into_callable(db)
.has_relation_to(db, target, relation)
}
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
// is an instance of its metaclass `abc.ABCMeta`.
@@ -1637,30 +1613,17 @@ impl<'db> Type<'db> {
/// Note: This function aims to have no false positives, but might return
/// wrong `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
let mut visitor = PairVisitor::new(false);
self.is_disjoint_from_impl(db, other, &mut visitor)
}
pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
fn any_protocol_members_absent_or_disjoint<'db>(
db: &'db dyn Db,
protocol: ProtocolInstanceType<'db>,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
protocol.interface(db).members(db).any(|member| {
other
.member(db, member.name())
.place
.ignore_possibly_unbound()
.is_none_or(|attribute_type| {
member.has_disjoint_type_from(db, attribute_type, visitor)
})
.is_none_or(|attribute_type| member.has_disjoint_type_from(db, attribute_type))
})
}
@@ -1694,19 +1657,19 @@ impl<'db> Type<'db> {
match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_disjoint_from_impl(db, other, visitor)
bound.is_disjoint_from(db, other)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_disjoint_from_impl(db, other, visitor)),
.all(|constraint| constraint.is_disjoint_from(db, other)),
}
}
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
.iter()
.all(|e| e.is_disjoint_from_impl(db, other, visitor)),
.all(|e| e.is_disjoint_from(db, other)),
// If we have two intersections, we test the positive elements of each one against the other intersection
// Negative elements need a positive element on the other side in order to be disjoint.
@@ -1715,11 +1678,11 @@ impl<'db> Type<'db> {
self_intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from_impl(db, other, visitor))
.any(|p| p.is_disjoint_from(db, other))
|| other_intersection
.positive(db)
.iter()
.any(|p: &Type<'_>| p.is_disjoint_from_impl(db, self, visitor))
.any(|p: &Type<'_>| p.is_disjoint_from(db, self))
}
(Type::Intersection(intersection), other)
@@ -1727,7 +1690,7 @@ impl<'db> Type<'db> {
intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from_impl(db, other, visitor))
.any(|p| p.is_disjoint_from(db, other))
// A & B & Not[C] is disjoint from C
|| intersection
.negative(db)
@@ -1841,17 +1804,17 @@ impl<'db> Type<'db> {
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.is_disjoint_from_impl(db, right, visitor)
left.is_disjoint_from(db, right)
}
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => {
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db), visitor)
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db))
}
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db), visitor)
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db))
}
// The absence of a protocol member on one of these types guarantees
@@ -1904,7 +1867,7 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(..)
| Type::GenericAlias(..)
| Type::IntLiteral(..)),
) => any_protocol_members_absent_or_disjoint(db, protocol, ty, visitor),
) => any_protocol_members_absent_or_disjoint(db, protocol, ty),
// This is the same as the branch above --
// once guard patterns are stabilised, it could be unified with that branch
@@ -1913,7 +1876,7 @@ impl<'db> Type<'db> {
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
if n.class.is_final(db) =>
{
any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor)
any_protocol_members_absent_or_disjoint(db, protocol, nominal)
}
(Type::ProtocolInstance(protocol), other)
@@ -1921,7 +1884,7 @@ impl<'db> Type<'db> {
protocol.interface(db).members(db).any(|member| {
matches!(
other.member(db, member.name()).place,
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor)
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type)
)
})
}
@@ -1944,18 +1907,18 @@ impl<'db> Type<'db> {
}
}
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from_impl(db, right),
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from(db, right),
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
// so although the type is dynamic we can still determine disjointedness in some situations
(Type::SubclassOf(subclass_of_ty), other)
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Dynamic(_) => {
KnownClass::Type.to_instance(db).is_disjoint_from_impl(db, other, visitor)
KnownClass::Type.to_instance(db).is_disjoint_from(db, other)
}
SubclassOfInner::Class(class) => class
.metaclass_instance_type(db)
.is_disjoint_from_impl(db, other, visitor),
.is_disjoint_from(db, other),
},
(Type::SpecialForm(special_form), Type::NominalInstance(instance))
@@ -2040,18 +2003,18 @@ impl<'db> Type<'db> {
(Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType
.to_instance(db)
.is_disjoint_from_impl(db, other, visitor),
.is_disjoint_from(db, other),
(Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => {
KnownClass::MethodWrapperType
.to_instance(db)
.is_disjoint_from_impl(db, other, visitor)
.is_disjoint_from(db, other)
}
(Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => {
KnownClass::WrapperDescriptorType
.to_instance(db)
.is_disjoint_from_impl(db, other, visitor)
.is_disjoint_from(db, other)
}
(Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_))
@@ -2113,15 +2076,15 @@ impl<'db> Type<'db> {
(Type::ModuleLiteral(..), other @ Type::NominalInstance(..))
| (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => {
// Modules *can* actually be instances of `ModuleType` subclasses
other.is_disjoint_from_impl(db, KnownClass::ModuleType.to_instance(db), visitor)
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
}
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.is_disjoint_from_impl(db, right)
left.is_disjoint_from(db, right)
}
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
tuple.is_disjoint_from_impl(db, other_tuple, visitor)
tuple.is_disjoint_from(db, other_tuple)
}
(Type::Tuple(tuple), Type::NominalInstance(instance))
@@ -2134,13 +2097,13 @@ impl<'db> Type<'db> {
(Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => {
KnownClass::Property
.to_instance(db)
.is_disjoint_from_impl(db, other, visitor)
.is_disjoint_from(db, other)
}
(Type::BoundSuper(_), Type::BoundSuper(_)) => !self.is_equivalent_to(db, other),
(Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => KnownClass::Super
.to_instance(db)
.is_disjoint_from_impl(db, other, visitor),
.is_disjoint_from(db, other),
}
}
@@ -3069,6 +3032,10 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
Type::AlwaysFalsy | Type::AlwaysTruthy => {
self.class_member_with_policy(db, name, policy)
}
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db,
name_str,
@@ -3090,8 +3057,6 @@ impl<'db> Type<'db> {
| Type::KnownInstance(..)
| Type::PropertyInstance(..)
| Type::FunctionLiteral(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(..) => {
let fallback = self.instance_member(db, name_str);
@@ -3531,6 +3496,7 @@ impl<'db> Type<'db> {
// For `builtins.property.__get__`, we use the same signature. The return types are not
// specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`.
let not_none = Type::none(db).negate(db);
CallableBinding::from_overloads(
self,
[
@@ -3546,7 +3512,7 @@ impl<'db> Type<'db> {
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(not_none),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@@ -3572,6 +3538,7 @@ impl<'db> Type<'db> {
// TODO: Consider merging this signature with the one in the previous match clause,
// since the previous one is just this signature with the `self` parameters
// removed.
let not_none = Type::none(db).negate(db);
let descriptor = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => {
KnownClass::FunctionType.to_instance(db)
@@ -3602,7 +3569,7 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(descriptor),
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(not_none),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@@ -7222,7 +7189,7 @@ impl<'db> BoundMethodType<'db> {
#[derive(PartialOrd, Ord)]
pub struct CallableType<'db> {
#[returns(ref)]
pub(crate) signatures: CallableSignature<'db>,
signatures: CallableSignature<'db>,
/// We use `CallableType` to represent function-like objects, like the synthesized methods
/// of dataclasses or NamedTuples. These callables act like real functions when accessed
@@ -7336,10 +7303,6 @@ impl<'db> CallableType<'db> {
///
/// See [`Type::is_equivalent_to`] for more details.
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
self.is_function_like(db) == other.is_function_like(db)
&& self
.signatures(db)

View File

@@ -730,6 +730,38 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::Overload) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.overload` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::Override) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.override` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::AbstractMethod) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `abc.abstractmethod` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::Final) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.final` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::GetattrStatic) => {
let [Some(instance_ty), Some(attr_name), default] =
overload.parameter_types()

View File

@@ -11,7 +11,7 @@ use super::{
};
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::place::NodeWithScopeKind;
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex, attribute_declarations};
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::function::{DataclassTransformerParams, KnownFunction};
@@ -1763,7 +1763,10 @@ impl<'db> ClassLiteral<'db> {
let class_map = use_def_map(db, class_body_scope);
let class_table = place_table(db, class_body_scope);
let is_valid_scope = |method_scope: ScopeId<'db>| {
for (attribute_assignments, method_scope_id) in
attribute_assignments(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
if let Some(method_def) = method_scope.node(db).as_function(&module) {
let method_name = method_def.name.as_str();
if let Place::Type(Type::FunctionLiteral(method_type), _) =
@@ -1771,53 +1774,10 @@ impl<'db> ClassLiteral<'db> {
{
let method_decorator = MethodDecorator::try_from_fn_type(db, method_type);
if method_decorator != Ok(target_method_decorator) {
return false;
continue;
}
}
}
true
};
// First check declarations
for (attribute_declarations, method_scope_id) in
attribute_declarations(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
if !is_valid_scope(method_scope) {
continue;
}
for attribute_declaration in attribute_declarations {
let DefinitionState::Defined(decl) = attribute_declaration.declaration else {
continue;
};
let DefinitionKind::AnnotatedAssignment(annotated) = decl.kind(db) else {
continue;
};
if use_def_map(db, method_scope)
.is_declaration_reachable(db, &attribute_declaration)
.is_always_false()
{
continue;
}
let annotation_ty =
infer_expression_type(db, index.expression(annotated.annotation(&module)));
return Place::bound(annotation_ty);
}
}
for (attribute_assignments, method_scope_id) in
attribute_assignments(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
if !is_valid_scope(method_scope) {
continue;
}
let method_map = use_def_map(db, method_scope);
// The attribute assignment inherits the reachability of the method which contains it
@@ -2055,7 +2015,6 @@ impl<'db> ClassLiteral<'db> {
let declarations = use_def.end_of_scope_declarations(place_id);
let declared_and_qualifiers = place_from_declarations(db, declarations);
match declared_and_qualifiers {
Ok(PlaceAndQualifiers {
place: mut declared @ Place::Type(declared_ty, declaredness),

View File

@@ -1,61 +1,25 @@
use rustc_hash::FxHashMap;
use crate::FxIndexSet;
use crate::types::Type;
use std::cmp::Eq;
use std::hash::Hash;
pub(crate) type TypeTransformer<'db> = CycleDetector<Type<'db>, Type<'db>>;
impl Default for TypeTransformer<'_> {
fn default() -> Self {
// TODO: proper recursive type handling
// This must be Any, not e.g. a todo type, because Any is the normalized form of the
// dynamic type (that is, todo types are normalized to Any).
CycleDetector::new(Type::any())
}
#[derive(Debug, Default)]
pub(crate) struct TypeTransformer<'db> {
seen: FxIndexSet<Type<'db>>,
}
pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>;
impl<'db> TypeTransformer<'db> {
pub(crate) fn visit(
&mut self,
ty: Type<'db>,
func: impl FnOnce(&mut Self) -> Type<'db>,
) -> Type<'db> {
if !self.seen.insert(ty) {
// TODO: proper recursive type handling
#[derive(Debug)]
pub(crate) struct CycleDetector<T, R> {
/// If the type we're visiting is present in `seen`,
/// it indicates that we've hit a cycle (due to a recursive type);
/// we need to immediately short circuit the whole operation and return the fallback value.
/// That's why we pop items off the end of `seen` after we've visited them.
seen: FxIndexSet<T>,
/// Unlike `seen`, this field is a pure performance optimisation (and an essential one).
/// If the type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit a cycle:
/// it just means that we've already visited this inner type as part of a bigger call chain we're currently in.
/// Since this cache is just a performance optimisation, it doesn't make sense to pop items off the end of the
/// cache after they've been visited (it would sort-of defeat the point of a cache if we did!)
cache: FxHashMap<T, R>,
fallback: R,
}
impl<T: Hash + Eq + Copy, R: Copy> CycleDetector<T, R> {
pub(crate) fn new(fallback: R) -> Self {
CycleDetector {
seen: FxIndexSet::default(),
cache: FxHashMap::default(),
fallback,
}
}
pub(crate) fn visit(&mut self, item: T, func: impl FnOnce(&mut Self) -> R) -> R {
if !self.seen.insert(item) {
return self.fallback;
}
if let Some(ty) = self.cache.get(&item) {
self.seen.pop();
return *ty;
// This must be Any, not e.g. a todo type, because Any is the normalized form of the
// dynamic type (that is, todo types are normalized to Any).
return Type::any();
}
let ret = func(self);
self.cache.insert(item, ret);
self.seen.pop();
ret
}

View File

@@ -2426,7 +2426,7 @@ fn report_invalid_base<'ctx, 'db>(
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.
/// have a `bar` member or submdoule.
///
/// If the `foo` module originates from the standard library and `foo.bar`
/// *does* exist as a submodule in the standard library on *other* Python

View File

@@ -767,8 +767,8 @@ impl<'db> FunctionType<'db> {
}
/// Convert the `FunctionType` into a [`Type::Callable`].
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(db, self.signature(db), false)
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
Type::Callable(CallableType::new(db, self.signature(db), false))
}
/// Convert the `FunctionType` into a [`Type::BoundMethod`].

View File

@@ -1,13 +1,10 @@
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
};
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
use crate::{Db, NameKind};
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
@@ -244,37 +241,3 @@ impl AllMembers {
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Name> {
AllMembers::of(db, ty).members
}
/// Get the primary definition kind for a name expression within a specific file.
/// Returns the first definition kind that is reachable for this name in its scope.
/// This is useful for IDE features like semantic tokens.
pub fn definition_kind_for_name<'db>(
db: &'db dyn Db,
file: File,
name: &ast::ExprName,
) -> Option<DefinitionKind<'db>> {
let index = semantic_index(db, file);
let name_str = name.id.as_str();
// Get the scope for this name expression
let file_scope = index.try_expression_scope_id(&ast::Expr::Name(name.clone()))?;
// Get the place table for this scope
let place_table = index.place_table(file_scope);
// Look up the place by name
let place_id = place_table.place_id_by_name(name_str)?;
// Get the use-def map and look up definitions for this place
let use_def_map = index.use_def_map(file_scope);
let declarations = use_def_map.all_reachable_declarations(place_id);
// Find the first valid definition and return its kind
for declaration in declarations {
if let Some(def) = declaration.declaration.definition() {
return Some(def.kind(db).clone());
}
}
None
}

View File

@@ -2122,9 +2122,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
node_index: _,
value,
}) => {
// If this is a call expression, we would have added a `ReturnsNever` constraint,
// meaning this will be a standalone expression.
self.infer_maybe_standalone_expression(value);
self.infer_expression(value);
}
ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement),
ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement),
@@ -3495,7 +3493,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
{
builder.into_diagnostic(format_args!(
"Can not assign object of type `{}` to attribute \
"Can not assign object of `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
@@ -5265,8 +5263,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let call_arguments = Self::parse_arguments(arguments);
let callable_type = self.infer_maybe_standalone_expression(func);
let callable_type = self.infer_expression(func);
if let Type::FunctionLiteral(function) = callable_type {
// Make sure that the `function.definition` is only called when the function is defined

View File

@@ -5,7 +5,6 @@ use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::PlaceAndQualifiers;
use crate::types::cyclic::PairVisitor;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::TupleType;
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance};
@@ -119,7 +118,7 @@ impl<'db> NominalInstanceType<'db> {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> bool {
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
!self.class.could_coexist_in_mro_with(db, other.class)
}
@@ -270,14 +269,7 @@ impl<'db> ProtocolInstanceType<'db> {
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
let self_normalized = self.normalized(db);
if self_normalized == Type::ProtocolInstance(other) {
return true;
}
self_normalized == other.normalized(db)
self.normalized(db) == other.normalized(db)
}
/// Return `true` if this protocol type is disjoint from the protocol `other`.
@@ -285,12 +277,7 @@ impl<'db> ProtocolInstanceType<'db> {
/// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y`
/// have a member with the same name but disjoint types
#[expect(clippy::unused_self)]
pub(super) fn is_disjoint_from_impl(
self,
_db: &'db dyn Db,
_other: Self,
_visitor: &mut PairVisitor<'db>,
) -> bool {
pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
false
}

View File

@@ -3,7 +3,7 @@ use crate::semantic_index::expression::Expression;
use crate::semantic_index::place::{PlaceExpr, PlaceTable, ScopeId, ScopedPlaceId};
use crate::semantic_index::place_table;
use crate::semantic_index::predicate::{
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
};
use crate::types::function::KnownFunction;
use crate::types::infer::infer_same_file_expression_type;
@@ -59,7 +59,6 @@ pub(crate) fn infer_narrowing_constraint<'db>(
all_negative_narrowing_constraints_for_pattern(db, pattern)
}
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(constraints) = constraints {
@@ -347,7 +346,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
PredicateNode::Pattern(pattern) => {
self.evaluate_pattern_predicate(pattern, self.is_positive)
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(mut constraints) = constraints {
@@ -431,9 +429,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
match self.predicate {
PredicateNode::Expression(expression) => expression.scope(self.db),
PredicateNode::Pattern(pattern) => pattern.scope(self.db),
PredicateNode::ReturnsNever(CallableAndCallExpr { callable, .. }) => {
callable.scope(self.db)
}
PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db),
}
}

View File

@@ -218,7 +218,6 @@ mod flaky {
use itertools::Itertools;
use super::{intersection, union};
use crate::types::{KnownClass, Type};
// Negating `T` twice is equivalent to `T`.
type_property_test!(
@@ -312,14 +311,4 @@ mod flaky {
bottom_materialization_of_type_is_assigneble_to_type, db,
forall types t. t.bottom_materialization(db).is_assignable_to(db, t)
);
// Any type assignable to `Iterable[object]` should be considered iterable.
//
// Note that the inverse is not true, due to the fact that we recognize the old-style
// iteration protocol as well as the new-style iteration protocol: not all objects that
// we consider iterable are assignable to `Iterable[object]`.
type_property_test!(
all_type_assignable_to_iterable_are_iterable, db,
forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object(db)])) => t.try_iterate(db).is_ok()
);
}

View File

@@ -55,10 +55,6 @@ pub(crate) enum Ty {
params: CallableParams,
returns: Option<Box<Ty>>,
},
/// `unittest.mock.Mock` is interesting because it is a nominal instance type
/// where the class has `Any` in its MRO
UnittestMockInstance,
UnittestMockLiteral,
}
#[derive(Debug, Clone, PartialEq)]
@@ -148,13 +144,6 @@ impl Ty {
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s)
.place
.expect_type(),
Ty::UnittestMockLiteral => known_module_symbol(db, KnownModule::UnittestMock, "Mock")
.place
.expect_type(),
Ty::UnittestMockInstance => Ty::UnittestMockLiteral
.into_type(db)
.to_instance(db)
.unwrap(),
Ty::TypingLiteral => Type::SpecialForm(SpecialFormType::Literal),
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).place.expect_type(),
Ty::KnownClassInstance(known_class) => known_class.to_instance(db),
@@ -234,13 +223,11 @@ fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty {
let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g));
// Update this if new non-fully-static types are added below.
let fully_static_index = 5;
let fully_static_index = 3;
let types = &[
Ty::Any,
Ty::Unknown,
Ty::SubclassOfAny,
Ty::UnittestMockLiteral,
Ty::UnittestMockInstance,
// Add fully static types below, dynamic types above.
// Update `fully_static_index` above if adding new dynamic types!
Ty::Never,

View File

@@ -11,7 +11,6 @@ use crate::{
types::{
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
Type, TypeMapping, TypeQualifiers, TypeRelation, TypeTransformer, TypeVarInstance,
cyclic::PairVisitor,
signatures::{Parameter, Parameters},
},
};
@@ -260,7 +259,7 @@ impl<'db> ProtocolMemberData<'db> {
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)]
enum ProtocolMemberKind<'db> {
Method(CallableType<'db>),
Method(Type<'db>), // TODO: use CallableType
Property(PropertyInstanceType<'db>),
Other(Type<'db>),
}
@@ -335,7 +334,7 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>(
visitor: &mut V,
) {
match member.kind {
ProtocolMemberKind::Method(method) => visitor.visit_callable_type(db, method),
ProtocolMemberKind::Method(method) => visitor.visit_type(db, method),
ProtocolMemberKind::Property(property) => {
visitor.visit_property_instance_type(db, property);
}
@@ -354,24 +353,17 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
fn ty(&self) -> Type<'db> {
match &self.kind {
ProtocolMemberKind::Method(callable) => Type::Callable(*callable),
ProtocolMemberKind::Method(callable) => *callable,
ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property),
ProtocolMemberKind::Other(ty) => *ty,
}
}
pub(super) fn has_disjoint_type_from(
&self,
db: &'db dyn Db,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
pub(super) fn has_disjoint_type_from(&self, db: &'db dyn Db, other: Type<'db>) -> bool {
match &self.kind {
// TODO: implement disjointness for property/method members as well as attribute members
ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false,
ProtocolMemberKind::Other(ty) => {
visitor.visit((*ty, other), |v| ty.is_disjoint_from_impl(db, other, v))
}
ProtocolMemberKind::Other(ty) => ty.is_disjoint_from(db, other),
}
}
@@ -508,10 +500,13 @@ fn cached_protocol_interface<'db>(
(Type::Callable(callable), BoundOnClass::Yes)
if callable.is_function_like(db) =>
{
ProtocolMemberKind::Method(callable)
ProtocolMemberKind::Method(ty)
}
(Type::FunctionLiteral(function), BoundOnClass::Yes) => {
ProtocolMemberKind::Method(function.into_callable_type(db))
// TODO: method members that have `FunctionLiteral` types should be upcast
// to `CallableType` so that two protocols with identical method members
// are recognized as equivalent.
(Type::FunctionLiteral(_function), BoundOnClass::Yes) => {
ProtocolMemberKind::Method(ty)
}
_ => ProtocolMemberKind::Other(ty),
};

Some files were not shown because too many files have changed in this diff Show More