Compare commits

..

4 Commits

Author SHA1 Message Date
Micha Reiser
2f6b96f2c5 [ty] Skip literal branches in union builder if it contains no literals 2025-12-18 09:39:33 +01:00
Micha Reiser
eecc160d18 More lazy negated computations 2025-12-18 09:17:00 +01:00
Micha Reiser
537deabc72 Defer insertion 2025-12-18 09:01:08 +01:00
Micha Reiser
a633f320d7 [ty] Small union builder nits 2025-12-18 08:17:56 +01:00
1187 changed files with 7077 additions and 16432 deletions

View File

@@ -1,67 +0,0 @@
{
"permissions": {
"allow": [
"Bash(cargo build:*)",
"Read(//home/micha/astral/discord.py/discord/**)",
"Bash(source:*)",
"Bash(./target/profiling/ty check:*)",
"Bash(tee:*)",
"Read(//home/micha/astral/discord.py/.venv/**)",
"Read(//home/micha/astral/discord.py/**)",
"Bash(perf record:*)",
"Bash(perf report:*)",
"Bash(time:*)",
"Bash(../ruff/target/profiling/ty check discord/audit_logs.py -vv)",
"Bash(sed:*)",
"Read(//home/micha/git/TypeScript/**)",
"Bash(cargo test:*)",
"Bash(MDTEST_TEST_FILTER='union_types.md - Union types - Unions of tuples' cargo test -p ty_python_semantic --test mdtest -- mdtest__union_types)",
"Bash(timeout 10 cargo test --package ty_python_semantic --lib types::tests::divergent_type)",
"Bash(timeout 30 cargo test:*)",
"Bash(git stash:*)",
"Bash(timeout 60 time:*)",
"Bash(for i in 1 2 3 4 5)",
"Bash(do echo \"Run $i:\")",
"Bash(done)",
"Bash(for i in 1 2 3)",
"Bash(cargo fuzz run:*)",
"Bash(timeout 60 cargo fuzz run -s none ty_check_invalid_syntax -- -timeout=1)",
"Bash(for:*)",
"Bash(do echo \"=== Checking $crash ===\")",
"Bash(uvx ty@latest check \"$crash\")",
"Bash(do echo \"=== $crash ===\")",
"Bash(while read crash)",
"Bash(cargo fuzz cmin:*)",
"Bash(cargo +nightly fuzz cmin:*)",
"Bash(timeout 120 cargo fuzz run -s none ty_check_invalid_syntax -- -timeout=1)",
"Bash(awk:*)",
"Bash(while read file)",
"Bash(cat:*)",
"Bash(uvx ty@latest:*)",
"Bash(do cp \"$crash\" /tmp/isolated_crash.py)",
"Bash(echo \"=== $crash ===\")",
"Bash(do echo \"=== test$i.py ===\")",
"Bash(do echo \"=== Testing $crash ===\")",
"Bash(tree:*)",
"Bash(cut:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(xargs basename:*)",
"Bash(wc:*)",
"Bash(find:*)",
"Bash({} ;)",
"Bash(git checkout:*)",
"Bash(do)",
"Bash(if ! grep -q \"use crate::types\" \"$f\")",
"Bash(! grep -q \"crate::types::\" \"$f\")",
"Bash(then)",
"Bash(else)",
"Bash(fi)",
"Bash(1)",
"Bash(__NEW_LINE__ echo \"Done\")",
"Bash(rm:*)"
],
"deny": [],
"ask": []
}
}

1
.gitattributes vendored
View File

@@ -22,7 +22,6 @@ crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf
crates/ruff_python_parser/resources/inline linguist-generated=true
crates/ty_python_semantic/resources/mdtest/external/*.lock linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf
ty.schema.json -diff linguist-generated=true text=auto eol=lf

View File

@@ -4,12 +4,10 @@
self-hosted-runner:
# Various runners we use that aren't recognized out-of-the-box by actionlint:
labels:
- depot-ubuntu-24.04-4
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- depot-windows-2022-16
- depot-ubuntu-22.04-arm-4
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16
- codspeed-macro

View File

@@ -4,6 +4,5 @@
# Enable off-by-default rules.
[rules]
possibly-unresolved-reference = "warn"
possibly-missing-import = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -952,7 +952,7 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
@@ -960,9 +960,9 @@ jobs:
mode: simulation
run: cargo codspeed run
benchmarks-instrumented-ty-build:
name: "benchmarks instrumented ty (build)"
runs-on: depot-ubuntu-24.04-4
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
@@ -971,6 +971,9 @@ jobs:
needs.determine_changes.outputs.ty == 'true'
)
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -980,6 +983,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
@@ -990,64 +994,28 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build -m instrumentation --features "codspeed,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Upload benchmark binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmarks-instrumented-ty-binary
path: target/codspeed/simulation/ruff_benchmark
retention-days: 1
benchmarks-instrumented-ty-run:
name: "benchmarks instrumented ty (${{ matrix.benchmark }})"
runs-on: ubuntu-24.04
needs: benchmarks-instrumented-ty-build
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
fail-fast: false
matrix:
benchmark:
- "check_file|micro|anyio"
- "attrs|hydra|datetype"
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
- name: "Download benchmark binary"
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v4.3.0
with:
name: benchmarks-instrumented-ty-binary
path: target/codspeed/simulation/ruff_benchmark
- name: "Restore binary permissions"
run: chmod +x target/codspeed/simulation/ruff_benchmark/ty
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
with:
mode: simulation
run: cargo codspeed run --bench ty "${{ matrix.benchmark }}"
run: cargo codspeed run
benchmarks-walltime-build:
name: "benchmarks walltime (build)"
# We only run this job if `github.repository == 'astral-sh/ruff'`,
# so hardcoding depot here is fine
runs-on: depot-ubuntu-22.04-arm-4
benchmarks-walltime:
name: "benchmarks walltime (${{ matrix.benchmarks }})"
runs-on: codspeed-macro
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
matrix:
benchmarks:
- "medium|multithreaded"
- "small|large"
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -1068,51 +1036,7 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build -m walltime --features "codspeed,ty_walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Upload benchmark binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmarks-walltime-binary
path: target/codspeed/walltime/ruff_benchmark
retention-days: 1
benchmarks-walltime-run:
name: "benchmarks walltime (${{ matrix.benchmark }})"
runs-on: codspeed-macro
needs: benchmarks-walltime-build
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
matrix:
benchmark:
- colour_science
- "pandas|tanjun|altair"
- "static_frame|sympy"
- "pydantic|multithreaded|freqtrade"
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
- name: "Download benchmark binary"
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: benchmarks-walltime-binary
path: target/codspeed/walltime/ruff_benchmark
- name: "Restore binary permissions"
run: chmod +x target/codspeed/walltime/ruff_benchmark/ty_walltime
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
@@ -1123,4 +1047,4 @@ jobs:
CODSPEED_PERF_ENABLED: false
with:
mode: walltime
run: cargo codspeed run --bench ty_walltime -m walltime "${{ matrix.benchmark }}"
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"

View File

@@ -62,7 +62,7 @@ jobs:
name: Create an issue if the daily fuzz surfaced any bugs
runs-on: ubuntu-latest
needs: fuzz
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.fuzz.result != 'success' }}
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.fuzz.result == 'failure' }}
permissions:
issues: write
steps:

View File

@@ -6,11 +6,6 @@ on:
pull_request:
paths:
- "crates/ty*/**"
- "!crates/ty_ide/**"
- "!crates/ty_server/**"
- "!crates/ty_test/**"
- "!crates/ty_completion_eval/**"
- "!crates/ty_wasm/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"

View File

@@ -16,7 +16,8 @@ name: Sync typeshed
# 3. Once the Windows worker is done, a MacOS worker:
# a. Checks out the branch created by the Linux worker
# b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
# c. Formats the code again
# c. Attempts to update any snapshots that might have changed
# (this sub-step is allowed to fail)
# d. Commits the changes and pushes them to the same upstream branch
# e. Creates a PR against the `main` branch using the branch all three workers have pushed to
# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository
@@ -197,6 +198,42 @@ jobs:
run: |
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: "Install Rust toolchain"
if: ${{ success() }}
run: rustup show
- name: "Install mold"
if: ${{ success() }}
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-insta
- name: Update snapshots
if: ${{ success() }}
run: |
cargo r \
--profile=profiling \
-p ty_completion_eval \
-- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
#
# If there were only snapshot-related failures, `cargo insta test --accept` will have exit code 0,
# but if there were also other mdtest failures (for example), it will return a nonzero exit code.
# We don't care about other tests failing here, we just want snapshots updated where possible,
# so we use `|| true` here to ignore the exit code.
cargo insta test --accept --color=always --all-features --test-runner=nextest || true
- name: Commit snapshot changes
if: ${{ success() }}
run: git commit -am "Update snapshots" || echo "No snapshot changes to commit"
- name: Push changes upstream and create a PR
if: ${{ success() }}
run: |
@@ -208,7 +245,7 @@ jobs:
name: Create an issue if the typeshed sync failed
runs-on: ubuntu-latest
needs: [sync, docstrings-windows, docstrings-macos-and-pr]
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && (needs.sync.result != 'success' || needs.docstrings-windows.result != 'success' || needs.docstrings-macos-and-pr.result != 'success') }}
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && (needs.sync.result == 'failure' || needs.docstrings-windows.result == 'failure' || needs.docstrings-macos-and-pr.result == 'failure') }}
permissions:
issues: write
steps:

View File

@@ -17,6 +17,7 @@ env:
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
REF_NAME: ${{ github.ref_name }}
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-analyzer:
@@ -111,13 +112,22 @@ jobs:
cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY"
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
# Make sure to update the bot if you rename the artifact.
- name: "Upload full report"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
name: full-report
path: dist/
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=ty-ecosystem --branch ${{ github.head_ref }} --commit-hash ${GITHUB_SHA}
- name: "Append deployment URL"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
run: |
echo >> comment.md
echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)** ([timing results]($DEPLOYMENT_URL/timing))" >> comment.md
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
# Make sure to update the bot if you rename the artifact.

View File

@@ -14,6 +14,7 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-report:
@@ -30,12 +31,12 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: true
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- name: Install Rust toolchain
run: rustup show
@@ -69,10 +70,11 @@ jobs:
ecosystem-diagnostics.json \
--output dist/index.html
# NOTE: astral-sh-bot uses this artifact to publish the ecosystem report.
# Make sure to update the bot if you rename the artifact.
- name: "Upload ecosystem report"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
name: full-report
path: dist/
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=ty-ecosystem --branch main --commit-hash ${GITHUB_SHA}

View File

@@ -6,11 +6,6 @@ on:
pull_request:
paths:
- "crates/ty*/**"
- "!crates/ty_ide/**"
- "!crates/ty_server/**"
- "!crates/ty_test/**"
- "!crates/ty_completion_eval/**"
- "!crates/ty_wasm/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"

View File

@@ -5,7 +5,7 @@ exclude: |
.github/workflows/release.yml|
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty_python_types/resources/corpus/.*|
crates/ty_python_semantic/resources/corpus/.*|
crates/ty/docs/(configuration|rules|cli|environment).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|

View File

@@ -1,54 +1,5 @@
# Changelog
## 0.14.10
Released on 2025-12-18.
### Preview features
- [formatter] Fluent formatting of method chains ([#21369](https://github.com/astral-sh/ruff/pull/21369))
- [formatter] Keep lambda parameters on one line and parenthesize the body if it expands ([#21385](https://github.com/astral-sh/ruff/pull/21385))
- \[`flake8-implicit-str-concat`\] New rule to prevent implicit string concatenation in collections (`ISC004`) ([#21972](https://github.com/astral-sh/ruff/pull/21972))
- \[`flake8-use-pathlib`\] Make fixes unsafe when types change in compound statements (`PTH104`, `PTH105`, `PTH109`, `PTH115`) ([#22009](https://github.com/astral-sh/ruff/pull/22009))
- \[`refurb`\] Extend support for `Path.open` (`FURB101`, `FURB103`) ([#21080](https://github.com/astral-sh/ruff/pull/21080))
### Bug fixes
- \[`pyupgrade`\] Fix parsing named Unicode escape sequences (`UP032`) ([#21901](https://github.com/astral-sh/ruff/pull/21901))
### Rule changes
- \[`eradicate`\] Ignore `ruff:disable` and `ruff:enable` comments in `ERA001` ([#22038](https://github.com/astral-sh/ruff/pull/22038))
- \[`flake8-pytest-style`\] Allow `match` and `check` keyword arguments without an expected exception type (`PT010`) ([#21964](https://github.com/astral-sh/ruff/pull/21964))
- [syntax-errors] Annotated name cannot be global ([#20868](https://github.com/astral-sh/ruff/pull/20868))
### Documentation
- Add `uv` and `ty` to the Ruff README ([#21996](https://github.com/astral-sh/ruff/pull/21996))
- Document known lambda formatting deviations from Black ([#21954](https://github.com/astral-sh/ruff/pull/21954))
- Update `setup.md` ([#22024](https://github.com/astral-sh/ruff/pull/22024))
- \[`flake8-bandit`\] Fix broken link (`S704`) ([#22039](https://github.com/astral-sh/ruff/pull/22039))
### Other changes
- Fix playground Share button showing "Copied!" before clipboard copy completes ([#21942](https://github.com/astral-sh/ruff/pull/21942))
### Contributors
- [@dylwil3](https://github.com/dylwil3)
- [@charliecloudberry](https://github.com/charliecloudberry)
- [@charliermarsh](https://github.com/charliermarsh)
- [@chirizxc](https://github.com/chirizxc)
- [@ntBre](https://github.com/ntBre)
- [@zanieb](https://github.com/zanieb)
- [@amyreese](https://github.com/amyreese)
- [@hauntsaninja](https://github.com/hauntsaninja)
- [@11happy](https://github.com/11happy)
- [@mahiro72](https://github.com/mahiro72)
- [@MichaReiser](https://github.com/MichaReiser)
- [@phongddo](https://github.com/phongddo)
- [@PeterJCLaw](https://github.com/PeterJCLaw)
## 0.14.9
Released on 2025-12-11.

136
Cargo.lock generated
View File

@@ -1004,6 +1004,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dir-test"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79"
dependencies = [
"dir-test-macros",
]
[[package]]
name = "dir-test-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e"
dependencies = [
"glob",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dirs"
version = "6.0.0"
@@ -2887,7 +2908,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"anyhow",
"argfile",
@@ -3083,7 +3104,6 @@ dependencies = [
"ty",
"ty_project",
"ty_python_semantic",
"ty_python_types",
"ty_static",
"url",
]
@@ -3130,9 +3150,7 @@ dependencies = [
"salsa",
"schemars",
"serde",
"ty_module_resolver",
"ty_python_semantic",
"ty_python_types",
"zip",
]
@@ -3148,7 +3166,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3507,7 +3525,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3622,8 +3640,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ce80691fa0b87dc2fd2235a26544e63e5e43d8d3#ce80691fa0b87dc2fd2235a26544e63e5e43d8d3"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [
"boxcar",
"compact_str",
@@ -3647,13 +3665,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ce80691fa0b87dc2fd2235a26544e63e5e43d8d3#ce80691fa0b87dc2fd2235a26544e63e5e43d8d3"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
[[package]]
name = "salsa-macros"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ce80691fa0b87dc2fd2235a26544e63e5e43d8d3#ce80691fa0b87dc2fd2235a26544e63e5e43d8d3"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [
"proc-macro2",
"quote",
@@ -4378,7 +4396,6 @@ dependencies = [
"tracing-flame",
"tracing-subscriber",
"ty_combine",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"ty_server",
@@ -4393,6 +4410,7 @@ dependencies = [
"ordermap",
"ruff_db",
"ruff_python_ast",
"ty_python_semantic",
]
[[package]]
@@ -4410,8 +4428,8 @@ dependencies = [
"tempfile",
"toml",
"ty_ide",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"walkdir",
]
@@ -4441,33 +4459,8 @@ dependencies = [
"salsa",
"smallvec",
"tracing",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"ty_python_types",
"ty_vendored",
]
[[package]]
name = "ty_module_resolver"
version = "0.0.0"
dependencies = [
"anyhow",
"camino",
"compact_str",
"get-size2",
"insta",
"ruff_db",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_stdlib",
"rustc-hash",
"salsa",
"strum",
"strum_macros",
"tempfile",
"thiserror 2.0.17",
"tracing",
"ty_vendored",
]
@@ -4505,9 +4498,7 @@ dependencies = [
"toml",
"tracing",
"ty_combine",
"ty_module_resolver",
"ty_python_semantic",
"ty_python_types",
"ty_static",
"ty_vendored",
]
@@ -4521,12 +4512,19 @@ dependencies = [
"bitvec",
"camino",
"colored 3.0.0",
"compact_str",
"dir-test",
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.16.1",
"indexmap",
"indoc",
"insta",
"itertools 0.14.0",
"memchr",
"ordermap",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"ruff_annotate_snippets",
@@ -4536,7 +4534,9 @@ dependencies = [
"ruff_macros",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
@@ -4550,57 +4550,10 @@ dependencies = [
"strsim",
"strum",
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.17",
"tracing",
"ty_combine",
"ty_module_resolver",
"ty_static",
"ty_vendored",
]
[[package]]
name = "ty_python_types"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"camino",
"compact_str",
"datatest-stable",
"drop_bomb",
"get-size2",
"glob",
"indexmap",
"indoc",
"insta",
"itertools 0.14.0",
"memchr",
"ordermap",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"ruff_db",
"ruff_diagnostics",
"ruff_macros",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"salsa",
"schemars",
"serde",
"serde_json",
"smallvec",
"static_assertions",
"strum",
"strum_macros",
"test-case",
"tracing",
"ty_module_resolver",
"ty_python_semantic",
"ty_static",
"ty_test",
@@ -4640,7 +4593,6 @@ dependencies = [
"tracing-subscriber",
"ty_combine",
"ty_ide",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
]
@@ -4681,9 +4633,7 @@ dependencies = [
"thiserror 2.0.17",
"toml",
"tracing",
"ty_module_resolver",
"ty_python_semantic",
"ty_python_types",
"ty_static",
"ty_vendored",
]

View File

@@ -45,10 +45,8 @@ ty = { path = "crates/ty" }
ty_combine = { path = "crates/ty_combine" }
ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_module_resolver = { path = "crates/ty_module_resolver" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_python_types = { path = "crates/ty_python_types" }
ty_server = { path = "crates/ty_server" }
ty_static = { path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
@@ -84,6 +82,7 @@ criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
datatest-stable = { version = "0.3.3" }
dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
etcetera = { version = "0.11.0" }
@@ -148,7 +147,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ce80691fa0b87dc2fd2235a26544e63e5e43d8d3", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -150,8 +150,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.10/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.9/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -184,7 +184,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.14.9
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.10"
version = "0.14.9"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -14,6 +14,6 @@ info:
success: false
exit_code: 1
----- stdout -----
::error title=Ruff (unformatted),file=[TMP]/input.py,line=1,endLine=2::input.py:1:1: unformatted: File would be reformatted
::error title=Ruff (unformatted),file=[TMP]/input.py,line=1,col=1,endLine=2,endColumn=1::input.py:1:1: unformatted: File would be reformatted
----- stderr -----

View File

@@ -19,32 +19,32 @@ doctest = false
[[bench]]
name = "linter"
harness = false
required-features = ["ruff_instrumented"]
required-features = ["instrumented"]
[[bench]]
name = "lexer"
harness = false
required-features = ["ruff_instrumented"]
required-features = ["instrumented"]
[[bench]]
name = "parser"
harness = false
required-features = ["ruff_instrumented"]
required-features = ["instrumented"]
[[bench]]
name = "formatter"
harness = false
required-features = ["ruff_instrumented"]
required-features = ["instrumented"]
[[bench]]
name = "ty"
harness = false
required-features = ["ty_instrumented"]
required-features = ["instrumented"]
[[bench]]
name = "ty_walltime"
harness = false
required-features = ["ty_walltime"]
required-features = ["walltime"]
[dependencies]
ruff_db = { workspace = true, features = ["testing"] }
@@ -67,32 +67,25 @@ tracing = { workspace = true }
workspace = true
[features]
default = ["ty_instrumented", "ty_walltime", "ruff_instrumented"]
# Enables the ruff instrumented benchmarks
ruff_instrumented = [
default = ["instrumented", "walltime"]
# Enables the benchmark that should only run with codspeed's instrumented runner
instrumented = [
"criterion",
"ruff_linter",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"mimalloc",
"tikv-jemallocator",
]
# Enables the ty instrumented benchmarks
ty_instrumented = [
"criterion",
"ty_project",
"ruff_python_trivia",
]
codspeed = ["codspeed-criterion-compat"]
# Enables the ty_walltime benchmarks
ty_walltime = ["ruff_db/os", "ty_project", "divan"]
# Enables benchmark that should only run with codspeed's walltime runner.
walltime = ["ruff_db/os", "ty_project", "divan"]
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dev-dependencies]
mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true, optional = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true }
[dev-dependencies]
rustc-hash = { workspace = true }

View File

@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13106,
13100,
);
static TANJUN: Benchmark = Benchmark::new(
@@ -235,55 +235,30 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
});
}
#[bench(sample_size = 2, sample_count = 3)]
fn altair(bencher: Bencher) {
run_single_threaded(bencher, &ALTAIR);
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(sample_size = 2, sample_count = 3)]
fn freqtrade(bencher: Bencher) {
run_single_threaded(bencher, &FREQTRADE);
#[bench(args=[&COLOUR_SCIENCE, &PANDAS, &STATIC_FRAME], sample_size=1, sample_count=3)]
fn medium(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(sample_size = 2, sample_count = 3)]
fn tanjun(bencher: Bencher) {
run_single_threaded(bencher, &TANJUN);
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(sample_size = 2, sample_count = 3)]
fn pydantic(bencher: Bencher) {
run_single_threaded(bencher, &PYDANTIC);
}
#[bench(sample_size = 1, sample_count = 3)]
fn static_frame(bencher: Bencher) {
run_single_threaded(bencher, &STATIC_FRAME);
}
#[bench(sample_size = 1, sample_count = 2)]
fn colour_science(bencher: Bencher) {
run_single_threaded(bencher, &COLOUR_SCIENCE);
}
#[bench(sample_size = 1, sample_count = 2)]
fn pandas(bencher: Bencher) {
run_single_threaded(bencher, &PANDAS);
}
#[bench(sample_size = 1, sample_count = 2)]
fn sympy(bencher: Bencher) {
run_single_threaded(bencher, &SYMPY);
}
#[bench(sample_size = 3, sample_count = 8)]
fn multithreaded(bencher: Bencher) {
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
bencher
.with_inputs(|| ALTAIR.setup_iteration())
.with_inputs(|| benchmark.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, ALTAIR.project.name, ALTAIR.max_diagnostics);
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
db
})
});

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
#[cfg(any(feature = "ty_instrumented", feature = "ruff_instrumented"))]
#[cfg(feature = "instrumented")]
pub mod criterion;
pub mod real_world_projects;

View File

@@ -1,51 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
/// Signals a [`CancellationToken`] that it should be canceled.
#[derive(Debug, Clone)]
pub struct CancellationTokenSource {
cancelled: Arc<AtomicBool>,
}
impl Default for CancellationTokenSource {
fn default() -> Self {
Self::new()
}
}
impl CancellationTokenSource {
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
}
}
pub fn is_cancellation_requested(&self) -> bool {
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
}
/// Creates a new token that uses this source.
pub fn token(&self) -> CancellationToken {
CancellationToken {
cancelled: self.cancelled.clone(),
}
}
/// Requests cancellation for operations using this token.
pub fn cancel(&self) {
self.cancelled
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
/// Token signals whether an operation should be canceled.
#[derive(Debug, Clone)]
pub struct CancellationToken {
cancelled: Arc<AtomicBool>,
}
impl CancellationToken {
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
}
}

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, fmt::Formatter, path::Path, sync::Arc};
use std::{fmt::Formatter, path::Path, sync::Arc};
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
@@ -11,7 +11,6 @@ pub use self::render::{
ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::cancellation::CancellationToken;
use crate::{Db, files::File};
mod render;
@@ -411,6 +410,11 @@ impl Diagnostic {
self.id().is_invalid_syntax()
}
/// Returns the message body to display to the user.
pub fn body(&self) -> &str {
self.primary_message()
}
/// Returns the message of the first sub-diagnostic with a `Help` severity.
///
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
@@ -1308,8 +1312,6 @@ pub struct DisplayDiagnosticConfig {
show_fix_diff: bool,
/// The lowest applicability that should be shown when reporting diagnostics.
fix_applicability: Applicability,
cancellation_token: Option<CancellationToken>,
}
impl DisplayDiagnosticConfig {
@@ -1383,20 +1385,6 @@ impl DisplayDiagnosticConfig {
pub fn fix_applicability(&self) -> Applicability {
self.fix_applicability
}
pub fn with_cancellation_token(
mut self,
token: Option<CancellationToken>,
) -> DisplayDiagnosticConfig {
self.cancellation_token = token;
self
}
pub fn is_canceled(&self) -> bool {
self.cancellation_token
.as_ref()
.is_some_and(|token| token.is_cancelled())
}
}
impl Default for DisplayDiagnosticConfig {
@@ -1410,7 +1398,6 @@ impl Default for DisplayDiagnosticConfig {
show_fix_status: false,
show_fix_diff: false,
fix_applicability: Applicability::Safe,
cancellation_token: None,
}
}
}
@@ -1487,15 +1474,6 @@ pub enum ConciseMessage<'a> {
Custom(&'a str),
}
impl<'a> ConciseMessage<'a> {
pub fn to_str(&self) -> Cow<'a, str> {
match self {
ConciseMessage::MainDiagnostic(s) | ConciseMessage::Custom(s) => Cow::Borrowed(s),
ConciseMessage::Both { .. } => Cow::Owned(self.to_string()),
}
}
}
impl std::fmt::Display for ConciseMessage<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
@@ -1512,16 +1490,6 @@ impl std::fmt::Display for ConciseMessage<'_> {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ConciseMessage<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
/// A diagnostic message string.
///
/// This is, for all intents and purposes, equivalent to a `Box<str>`.

View File

@@ -52,7 +52,7 @@ impl AzureRenderer<'_> {
f,
"code={code};]{body}",
code = diag.secondary_code_or_id(),
body = diag.concise_message(),
body = diag.body(),
)?;
}

View File

@@ -28,10 +28,6 @@ impl<'a> ConciseRenderer<'a> {
let sep = fmt_styled(":", stylesheet.separator);
for diag in diagnostics {
if self.config.is_canceled() {
return Ok(());
}
if let Some(span) = diag.primary_span() {
write!(
f,

View File

@@ -53,10 +53,6 @@ impl<'a> FullRenderer<'a> {
.hyperlink(stylesheet.hyperlink);
for diag in diagnostics {
if self.config.is_canceled() {
return Ok(());
}
let resolved = Resolved::new(self.resolver, diag, self.config);
let renderable = resolved.to_renderable(self.config.context);
for diag in renderable.diagnostics.iter() {

View File

@@ -49,26 +49,14 @@ impl<'a> GithubRenderer<'a> {
}
.unwrap_or_default();
// GitHub Actions workflow commands have constraints on error annotations:
// - `col` and `endColumn` cannot be set if `line` and `endLine` are different
// See: https://github.com/astral-sh/ruff/issues/22074
if start_location.line == end_location.line {
write!(
f,
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
row = start_location.line,
column = start_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
} else {
write!(
f,
",line={row},endLine={end_row}::",
row = start_location.line,
end_row = end_location.line,
)?;
}
write!(
f,
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
row = start_location.line,
column = start_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
write!(
f,
@@ -87,7 +75,7 @@ impl<'a> GithubRenderer<'a> {
write!(f, "{id}:", id = diagnostic.id())?;
}
writeln!(f, " {}", diagnostic.concise_message())?;
writeln!(f, " {}", diagnostic.body())?;
}
Ok(())

View File

@@ -98,7 +98,7 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);
let description = diagnostic.concise_message();
let description = diagnostic.body();
let check_name = diagnostic.secondary_code_or_id();
let severity = match diagnostic.severity() {
Severity::Info => "info",

View File

@@ -6,7 +6,7 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed};
use ruff_text_size::Ranged;
use crate::diagnostic::{ConciseMessage, Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
use super::FileResolver;
@@ -101,7 +101,7 @@ pub(super) fn diagnostic_to_json<'a>(
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
message: diagnostic.concise_message(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
location: start_location.map(JsonLocation::from),
@@ -113,7 +113,7 @@ pub(super) fn diagnostic_to_json<'a>(
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
message: diagnostic.concise_message(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
location: Some(start_location.unwrap_or_default().into()),
@@ -226,7 +226,7 @@ pub(crate) struct JsonDiagnostic<'a> {
filename: Option<&'a str>,
fix: Option<JsonFix<'a>>,
location: Option<JsonLocation>,
message: ConciseMessage<'a>,
message: &'a str,
noqa_row: Option<OneIndexed>,
url: Option<&'a str>,
}

View File

@@ -56,17 +56,17 @@ impl<'a> JunitRenderer<'a> {
start_location: location,
} = diagnostic;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(diagnostic.concise_message().to_str());
status.set_message(diagnostic.body());
if let Some(location) = location {
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = diagnostic.concise_message()
body = diagnostic.body()
));
} else {
status.set_description(diagnostic.concise_message().to_str());
status.set_description(diagnostic.body());
}
let code = diagnostic

View File

@@ -55,7 +55,7 @@ impl PylintRenderer<'_> {
f,
"{path}:{row}: [{code}] {body}",
path = filename,
body = diagnostic.concise_message()
body = diagnostic.body()
)?;
}

View File

@@ -5,7 +5,7 @@ use ruff_diagnostics::{Edit, Fix};
use ruff_source_file::{LineColumn, SourceCode};
use ruff_text_size::Ranged;
use crate::diagnostic::{ConciseMessage, Diagnostic};
use crate::diagnostic::Diagnostic;
use super::FileResolver;
@@ -76,7 +76,7 @@ fn diagnostic_to_rdjson<'a>(
let edits = diagnostic.fix().map(Fix::edits).unwrap_or_default();
RdjsonDiagnostic {
message: diagnostic.concise_message(),
message: diagnostic.body(),
location,
code: RdjsonCode {
value: diagnostic
@@ -155,7 +155,7 @@ struct RdjsonDiagnostic<'a> {
code: RdjsonCode<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
location: Option<RdjsonLocation<'a>>,
message: ConciseMessage<'a>,
message: &'a str,
#[serde(skip_serializing_if = "Vec::is_empty")]
suggestions: Vec<RdjsonSuggestion<'a>>,
}

View File

@@ -2,5 +2,5 @@
source: crates/ruff_db/src/diagnostic/render/github.rs
expression: env.render_diagnostics(&diagnostics)
---
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=1,endLine=2::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=3,endLine=4::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline

View File

@@ -12,7 +12,6 @@ use std::hash::BuildHasherDefault;
use std::num::NonZeroUsize;
use ty_static::EnvVars;
pub mod cancellation;
pub mod diagnostic;
pub mod display;
pub mod file_revision;

View File

@@ -275,16 +275,16 @@ impl OsSystem {
/// instead of at least one system call for each component between `path` and `prefix`.
///
/// However, using `canonicalize` to resolve the path's casing doesn't work in two cases:
/// * if `path` is a symlink, `canonicalize` returns the symlink's target and not the symlink's source path.
/// * on Windows: If `path` is a mapped network drive, `canonicalize` returns the UNC path
/// (e.g. `Z:\` is mapped to `\\server\share` and `canonicalize` returns `\\?\UNC\server\share`).
/// * if `path` is a symlink because `canonicalize` then returns the symlink's target and not the symlink's source path.
/// * on Windows: If `path` is a mapped network drive because `canonicalize` then returns the UNC path
/// (e.g. `Z:\` is mapped to `\\server\share` and `canonicalize` then returns `\\?\UNC\server\share`).
///
/// Symlinks and mapped network drives should be rare enough that this fast path is worth trying first,
/// even if it comes at a cost for those rare use cases.
fn path_exists_case_sensitive_fast(&self, path: &SystemPath) -> Option<bool> {
// This is a more forgiving version of `dunce::simplified` that removes all `\\?\` prefixes on Windows.
// We use this more forgiving version because we don't intend on using either path for anything other than comparison
// and the prefix is only relevant when passing the path to other programs and it's longer than 200 something
// and the prefix is only relevant when passing the path to other programs and its longer than 200 something
// characters.
fn simplify_ignore_verbatim(path: &SystemPath) -> &SystemPath {
if cfg!(windows) {
@@ -298,7 +298,9 @@ impl OsSystem {
}
}
let Ok(canonicalized) = path.as_std_path().canonicalize() else {
let simplified = simplify_ignore_verbatim(path);
let Ok(canonicalized) = simplified.as_std_path().canonicalize() else {
// The path doesn't exist or can't be accessed. The path doesn't exist.
return Some(false);
};
@@ -307,13 +309,12 @@ impl OsSystem {
// The original path is valid UTF8 but the canonicalized path isn't. This definitely suggests
// that a symlink is involved. Fall back to the slow path.
tracing::debug!(
"Falling back to the slow case-sensitive path existence check because the canonicalized path of `{path}` is not valid UTF-8"
"Falling back to the slow case-sensitive path existence check because the canonicalized path of `{simplified}` is not valid UTF-8"
);
return None;
};
let simplified_canonicalized = simplify_ignore_verbatim(&canonicalized);
let simplified = simplify_ignore_verbatim(path);
// Test if the paths differ by anything other than casing. If so, that suggests that
// `path` pointed to a symlink (or some other none reversible path normalization happened).

View File

@@ -1,6 +1,5 @@
use glob::PatternError;
use ruff_notebook::{Notebook, NotebookError};
use rustc_hash::FxHashMap;
use std::panic::RefUnwindSafe;
use std::sync::{Arc, Mutex};
@@ -21,44 +20,18 @@ use super::walk_directory::WalkDirectoryBuilder;
///
/// ## Warning
/// Don't use this system for production code. It's intended for testing only.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct TestSystem {
inner: Arc<dyn WritableSystem + RefUnwindSafe + Send + Sync>,
/// Environment variable overrides. If a key is present here, it takes precedence
/// over the inner system's environment variables.
env_overrides: Arc<Mutex<FxHashMap<String, Option<String>>>>,
}
impl Clone for TestSystem {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
env_overrides: self.env_overrides.clone(),
}
}
}
impl TestSystem {
pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self {
Self {
inner: Arc::new(inner),
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
}
}
/// Sets an environment variable override. This takes precedence over the inner system.
pub fn set_env_var(&self, name: impl Into<String>, value: impl Into<String>) {
self.env_overrides
.lock()
.unwrap()
.insert(name.into(), Some(value.into()));
}
/// Removes an environment variable override, making it appear as not set.
pub fn remove_env_var(&self, name: impl Into<String>) {
self.env_overrides.lock().unwrap().insert(name.into(), None);
}
/// Returns the [`InMemorySystem`].
///
/// ## Panics
@@ -174,18 +147,6 @@ impl System for TestSystem {
self.system().case_sensitivity()
}
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
// Check overrides first
if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) {
return match override_value {
Some(value) => Ok(value.clone()),
None => Err(std::env::VarError::NotPresent),
};
}
// Fall back to inner system
self.system().env_var(name)
}
fn dyn_clone(&self) -> Box<dyn System> {
Box::new(self.clone())
}
@@ -195,7 +156,6 @@ impl Default for TestSystem {
fn default() -> Self {
Self {
inner: Arc::new(InMemorySystem::default()),
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
}
}
}

View File

@@ -14,7 +14,6 @@ license = { workspace = true }
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_python_semantic = { workspace = true }
ty_python_types = { workspace = true }
ty_static = { workspace = true }
ruff = { workspace = true }
ruff_formatter = { workspace = true }

View File

@@ -1,13 +1,11 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
use std::borrow::Cow;
use std::{fmt::Write, path::PathBuf};
use anyhow::bail;
use itertools::Itertools;
use pretty_assertions::StrComparison;
use std::{fmt::Write, path::PathBuf};
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_python_trivia::textwrap;
use ty_project::metadata::Options;
use crate::{
@@ -167,69 +165,62 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
let _ = writeln!(output, "**Default value**: `{}`", field.default);
output.push('\n');
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str(&format_example(
"pyproject.toml",
&format_header(
field.scope,
field.example,
parents,
ConfigurationFile::PyprojectToml,
),
field.example,
));
output.push('\n');
}
for configuration_file in [ConfigurationFile::PyprojectToml, ConfigurationFile::TyToml] {
let (header, example) =
format_snippet(field.scope, field.example, parents, configuration_file);
output.push_str(&format_tab(configuration_file.name(), &header, &example));
output.push('\n');
fn format_example(title: &str, header: &str, content: &str) -> String {
if header.is_empty() {
format!("```toml title=\"{title}\"\n{content}\n```\n",)
} else {
format!("```toml title=\"{title}\"\n{header}\n{content}\n```\n",)
}
}
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
let header = if header.is_empty() {
String::new()
} else {
format!("\n {header}")
};
format!(
"=== \"{}\"\n\n ```toml{}\n{}\n ```\n",
tab_name,
header,
textwrap::indent(content, " ")
)
}
/// Format the TOML header for the example usage for a given option.
///
/// For example: `[tool.ty.rules]`.
fn format_snippet<'a>(
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
fn format_header(
scope: Option<&str>,
example: &'a str,
example: &str,
parents: &[Set],
configuration: ConfigurationFile,
) -> (String, Cow<'a, str>) {
let mut example = Cow::Borrowed(example);
) -> String {
let tool_parent = match configuration {
ConfigurationFile::PyprojectToml => Some("tool.ty"),
ConfigurationFile::TyToml => None,
};
let header = configuration
.parent_table()
let header = tool_parent
.into_iter()
.chain(parents.iter().filter_map(|parent| parent.name()))
.chain(scope)
.join(".");
// Rewrite examples starting with `[tool.ty]` or `[[tool.ty]]` to their `ty.toml` equivalent.
if matches!(configuration, ConfigurationFile::TyToml) {
example = example.replace("[tool.ty.", "[").into();
}
// Ex) `[[tool.ty.xx]]`
if example.starts_with(&format!("[[{header}")) {
return (String::new(), example);
return String::new();
}
// Ex) `[tool.ty.rules]`
if example.starts_with(&format!("[{header}")) {
return (String::new(), example);
return String::new();
}
if header.is_empty() {
(String::new(), example)
String::new()
} else {
(format!("[{header}]"), example)
format!("[{header}]")
}
}
@@ -252,25 +243,10 @@ impl Visit for CollectOptionsVisitor {
#[derive(Debug, Copy, Clone)]
enum ConfigurationFile {
PyprojectToml,
#[expect(dead_code)]
TyToml,
}
impl ConfigurationFile {
const fn name(self) -> &'static str {
match self {
Self::PyprojectToml => "pyproject.toml",
Self::TyToml => "ty.toml",
}
}
const fn parent_table(self) -> Option<&'static str> {
match self {
Self::PyprojectToml => Some("tool.ty"),
Self::TyToml => None,
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -52,7 +52,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
}
fn generate_markdown() -> String {
let registry = ty_python_types::default_lint_registry();
let registry = ty_python_semantic::default_lint_registry();
let mut output = String::new();
@@ -63,7 +63,12 @@ fn generate_markdown() -> String {
let _ = writeln!(&mut output, "# Rules\n");
let mut lints: Vec<_> = registry.lints().iter().collect();
lints.sort_by_key(|a| a.name());
lints.sort_by(|a, b| {
a.default_level()
.cmp(&b.default_level())
.reverse()
.then_with(|| a.name().cmp(&b.name()))
});
for lint in lints {
let _ = writeln!(&mut output, "## `{rule_name}`\n", rule_name = lint.name());
@@ -114,7 +119,7 @@ fn generate_markdown() -> String {
let _ = writeln!(
&mut output,
r#"<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
{status_text} ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>

View File

@@ -16,9 +16,7 @@ ruff_linter = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true }
ty_python_types = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true, optional = true }

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::visitor::source_order::{
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
};
use ruff_python_ast::{self as ast, Expr, Mod, Stmt};
use ty_module_resolver::ModuleName;
use ty_python_semantic::ModuleName;
/// Collect all imports for a given Python file.
#[derive(Default, Debug)]

View File

@@ -7,13 +7,11 @@ use ruff_db::files::{File, Files};
use ruff_db::system::{OsSystem, System, SystemPathBuf};
use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder};
use ruff_python_ast::PythonVersion;
use ty_module_resolver::{SearchPathSettings, SearchPaths};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
AnalysisSettings, Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform,
PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin,
Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, default_lint_registry,
};
use ty_python_types::default_lint_registry;
static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::LazyLock::new(|| {
let mut builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored);
@@ -28,7 +26,6 @@ pub struct ModuleDb {
files: Files,
system: OsSystem,
rule_selection: Arc<RuleSelection>,
analysis_settings: Arc<AnalysisSettings>,
}
impl ModuleDb {
@@ -88,13 +85,6 @@ impl SourceDb for ModuleDb {
}
}
#[salsa::db]
impl ty_module_resolver::Db for ModuleDb {
fn search_paths(&self) -> &SearchPaths {
Program::get(self).search_paths(self)
}
}
#[salsa::db]
impl Db for ModuleDb {
fn should_check_file(&self, file: File) -> bool {
@@ -112,10 +102,6 @@ impl Db for ModuleDb {
fn verbose(&self) -> bool {
false
}
fn analysis_settings(&self) -> &AnalysisSettings {
&self.analysis_settings
}
}
#[salsa::db]

View File

@@ -1,6 +1,6 @@
use ruff_db::files::{File, FilePath, system_path_to_file};
use ruff_db::system::SystemPath;
use ty_module_resolver::{
use ty_python_semantic::{
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident,
};

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.10"
version = "0.14.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -9,15 +9,3 @@ def test_ok():
def test_error():
with pytest.raises(UnicodeError):
pass
def test_match_only():
with pytest.raises(match="some error message"):
pass
def test_check_only():
with pytest.raises(check=lambda e: True):
pass
def test_match_and_check():
with pytest.raises(match="some error message", check=lambda e: True):
pass

View File

@@ -86,26 +86,3 @@ def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")
def f():
# Unknown rule codes
# ruff: disable[YF829]
# ruff: disable[F841, RQW320]
value = 0
# ruff: enable[F841, RQW320]
# ruff: enable[YF829]
def f():
# External rule codes should be ignored
# ruff: disable[TK421]
print("hello")
# ruff: enable[TK421]
def f():
# Empty or missing rule codes
# ruff: disable
# ruff: disable[]
print("hello")

View File

@@ -1064,8 +1064,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,
(Ruff, "103") => rules::ruff::rules::InvalidSuppressionComment,
(Ruff, "104") => rules::ruff::rules::UnmatchedSuppressionComment,
(Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml,
#[cfg(any(feature = "test-rules", test))]

View File

@@ -197,7 +197,7 @@ impl Display for RuleCodeAndBody<'_> {
f,
"{fix}{body}",
fix = format_args!("[{}] ", "*".cyan()),
body = self.message.concise_message(),
body = self.message.body(),
);
}
}
@@ -208,14 +208,14 @@ impl Display for RuleCodeAndBody<'_> {
f,
"{code} {body}",
code = code.red().bold(),
body = self.message.concise_message(),
body = self.message.body(),
)
} else {
write!(
f,
"{code}: {body}",
code = self.message.id().as_str().red().bold(),
body = self.message.concise_message(),
body = self.message.body(),
)
}
}

View File

@@ -334,7 +334,7 @@ impl<'a> SarifResult<'a> {
rule_id: RuleCode::from(diagnostic),
level: "error".to_string(),
message: SarifMessage {
text: diagnostic.concise_message().to_string(),
text: diagnostic.body().to_string(),
},
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
locations: vec![SarifLocation {

View File

@@ -37,11 +37,10 @@ use crate::{Fix, FixAvailability, Violation};
/// import logging
///
/// logging.basicConfig(level=logging.INFO)
/// logger = logging.getLogger(__name__)
///
///
/// def sum_less_than_four(a, b):
/// logger.debug("Calling sum_less_than_four")
/// logging.debug("Calling sum_less_than_four")
/// return a + b < 4
/// ```
///

View File

@@ -125,9 +125,6 @@ impl Violation for PytestRaisesTooBroad {
/// ## Why is this bad?
/// `pytest.raises` expects to receive an expected exception as its first
/// argument. If omitted, the `pytest.raises` call will fail at runtime.
/// The rule will also accept calls without an expected exception but with
/// `match` and/or `check` keyword arguments, which are also valid after
/// pytest version 8.4.0.
///
/// ## Example
/// ```python
@@ -184,8 +181,6 @@ pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) {
.arguments
.find_argument("expected_exception", 0)
.is_none()
&& call.arguments.find_keyword("match").is_none()
&& call.arguments.find_keyword("check").is_none()
{
checker.report_diagnostic(PytestRaisesWithoutException, call.func.range());
}

View File

@@ -176,7 +176,7 @@ fn match_consecutive_appends<'a>(
let suite = if semantic.at_top_level() {
// If the statement is at the top level, we should go to the parent module.
// Module is available in the definitions list.
EnclosingSuite::new(semantic.definitions.python_ast()?, stmt.into())?
EnclosingSuite::new(semantic.definitions.python_ast()?, stmt)?
} else {
// Otherwise, go to the parent, and take its body as a sequence of siblings.
semantic

View File

@@ -313,20 +313,12 @@ mod tests {
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"]),
]),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"])
.with_preview_mode(),
);
Ok(())

View File

@@ -9,21 +9,6 @@ use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::{AlwaysFixableViolation, Edit, Fix};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InvalidRuleCodeKind {
Noqa,
Suppression,
}
impl InvalidRuleCodeKind {
fn as_str(&self) -> &str {
match self {
InvalidRuleCodeKind::Noqa => "`# noqa`",
InvalidRuleCodeKind::Suppression => "suppression",
}
}
}
/// ## What it does
/// Checks for `noqa` codes that are invalid.
///
@@ -51,17 +36,12 @@ impl InvalidRuleCodeKind {
#[violation_metadata(preview_since = "0.11.4")]
pub(crate) struct InvalidRuleCode {
pub(crate) rule_code: String,
pub(crate) kind: InvalidRuleCodeKind,
}
impl AlwaysFixableViolation for InvalidRuleCode {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Invalid rule code in {}: {}",
self.kind.as_str(),
self.rule_code
)
format!("Invalid rule code in `# noqa`: {}", self.rule_code)
}
fn fix_title(&self) -> String {
@@ -81,9 +61,7 @@ pub(crate) fn invalid_noqa_code(
continue;
};
let all_valid = directive
.iter()
.all(|code| code_is_valid(code.as_str(), external));
let all_valid = directive.iter().all(|code| code_is_valid(code, external));
if all_valid {
continue;
@@ -91,7 +69,7 @@ pub(crate) fn invalid_noqa_code(
let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive
.iter()
.partition(|&code| code_is_valid(code.as_str(), external));
.partition(|&code| code_is_valid(code, external));
if valid_codes.is_empty() {
all_codes_invalid_diagnostic(directive, invalid_codes, context);
@@ -103,9 +81,10 @@ pub(crate) fn invalid_noqa_code(
}
}
pub(crate) fn code_is_valid(code: &str, external: &[String]) -> bool {
Rule::from_code(get_redirect_target(code).unwrap_or(code)).is_ok()
|| external.iter().any(|ext| code.starts_with(ext))
fn code_is_valid(code: &Code, external: &[String]) -> bool {
let code_str = code.as_str();
Rule::from_code(get_redirect_target(code_str).unwrap_or(code_str)).is_ok()
|| external.iter().any(|ext| code_str.starts_with(ext))
}
fn all_codes_invalid_diagnostic(
@@ -121,7 +100,6 @@ fn all_codes_invalid_diagnostic(
.map(Code::as_str)
.collect::<Vec<_>>()
.join(", "),
kind: InvalidRuleCodeKind::Noqa,
},
directive.range(),
)
@@ -138,7 +116,6 @@ fn some_codes_are_invalid_diagnostic(
.report_diagnostic(
InvalidRuleCode {
rule_code: invalid_code.to_string(),
kind: InvalidRuleCodeKind::Noqa,
},
invalid_code.range(),
)

View File

@@ -1,59 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::AlwaysFixableViolation;
use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};
/// ## What it does
/// Checks for invalid suppression comments
///
/// ## Why is this bad?
/// Invalid suppression comments are ignored by Ruff, and should either
/// be fixed or removed to avoid confusion.
///
/// ## Example
/// ```python
/// ruff: disable # missing codes
/// ```
///
/// Use instead:
/// ```python
/// # ruff: disable[E501]
/// ```
///
/// Or delete the invalid suppression comment.
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.11")]
pub(crate) struct InvalidSuppressionComment {
pub(crate) kind: InvalidSuppressionCommentKind,
}
impl AlwaysFixableViolation for InvalidSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
let msg = match self.kind {
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Indentation) => {
"unexpected indentation".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Trailing) => {
"trailing comments are not supported".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Unmatched) => {
"no matching 'disable' comment".to_string()
}
InvalidSuppressionCommentKind::Error(error) => format!("{error}"),
};
format!("Invalid suppression comment: {msg}")
}
fn fix_title(&self) -> String {
"Remove suppression comment".to_string()
}
}
pub(crate) enum InvalidSuppressionCommentKind {
Invalid(InvalidSuppressionKind),
Error(ParseErrorKind),
}

View File

@@ -22,7 +22,6 @@ pub(crate) use invalid_formatter_suppression_comment::*;
pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use invalid_suppression_comment::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
@@ -47,7 +46,6 @@ pub(crate) use starmap_zip::*;
pub(crate) use static_key_dict_comprehension::*;
#[cfg(any(feature = "test-rules", test))]
pub(crate) use test_rules::*;
pub(crate) use unmatched_suppression_comment::*;
pub(crate) use unnecessary_cast_to_int::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
@@ -89,7 +87,6 @@ mod invalid_formatter_suppression_comment;
mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod invalid_suppression_comment;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
@@ -116,7 +113,6 @@ mod static_key_dict_comprehension;
mod suppression_comment_visitor;
#[cfg(any(feature = "test-rules", test))]
pub(crate) mod test_rules;
mod unmatched_suppression_comment;
mod unnecessary_cast_to_int;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;

View File

@@ -1,42 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
/// ## What it does
/// Checks for unmatched range suppression comments
///
/// ## Why is this bad?
/// Unmatched range suppression comments can inadvertently suppress violations
/// over larger sections of code than intended, particularly at module scope.
///
/// ## Example
/// ```python
/// def foo():
/// # ruff: disable[E501] # unmatched
/// REALLY_LONG_VALUES = [...]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// # ruff: disable[E501]
/// REALLY_LONG_VALUES = [...]
/// # ruff: enable[E501]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.11")]
pub(crate) struct UnmatchedSuppressionComment;
impl Violation for UnmatchedSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
"Suppression comment without matching `#ruff:enable` comment".to_string()
}
}

View File

@@ -6,8 +6,8 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs
+linter.preview = enabled
--- Summary ---
Removed: 15
Added: 23
Removed: 14
Added: 11
--- Removed ---
E741 Ambiguous variable name: `I`
@@ -238,60 +238,8 @@ help: Remove assignment to unused variable `I`
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `value` is assigned to but never used
--> suppressions.py:95:5
|
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
95 | value = 0
| ^^^^^
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
|
help: Remove assignment to unused variable `value`
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
- value = 0
95 + pass
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
98 |
note: This is an unsafe fix and may change runtime behavior
--- Added ---
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:11:5
|
9 | # These should both be ignored by the implicit range suppression.
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
12 | I = 1
|
RUF103 [*] Invalid suppression comment: no matching 'disable' comment
--> suppressions.py:19:5
|
17 | # should be generated.
18 | I = 1
19 | # ruff: enable[E741, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Remove suppression comment
16 | # Neither warning is ignored, and an "unmatched suppression"
17 | # should be generated.
18 | I = 1
- # ruff: enable[E741, F841]
19 |
20 |
21 | def f():
note: This is an unsafe fix and may change runtime behavior
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:46:5
|
@@ -350,17 +298,6 @@ help: Remove unused `noqa` directive
58 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:61:5
|
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | foo = 0
|
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:61:21
|
@@ -381,18 +318,6 @@ help: Remove unused suppression
64 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:68:5
|
66 | # Overlapping range suppressions, one should be marked as used,
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
| ^^^^^^^^^^^^^^^^^^^^^
69 | # ruff: disable[F841]
70 | foo = 0
|
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:69:5
|
@@ -412,17 +337,6 @@ help: Remove unused suppression
71 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:75:5
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 | foo = 0
|
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:75:21
|
@@ -463,17 +377,6 @@ help: Remove unused suppression
78 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:81:5
|
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | I = 0
|
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:81:27
|
@@ -510,8 +413,6 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[F401, F841]
88 | print("hello")
89 |
90 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
@@ -530,8 +431,6 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F841]
88 | print("hello")
89 |
90 |
RUF100 [*] Unused suppression (unused: `F841`)
@@ -550,122 +449,3 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F401]
88 | print("hello")
89 |
90 |
RUF102 [*] Invalid rule code in suppression: YF829
--> suppressions.py:93:21
|
91 | def f():
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
| ^^^^^
94 | # ruff: disable[F841, RQW320]
95 | value = 0
|
help: Remove the rule code
90 |
91 | def f():
92 | # Unknown rule codes
- # ruff: disable[YF829]
93 | # ruff: disable[F841, RQW320]
94 | value = 0
95 | # ruff: enable[F841, RQW320]
RUF102 [*] Invalid rule code in suppression: RQW320
--> suppressions.py:94:27
|
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
| ^^^^^^
95 | value = 0
96 | # ruff: enable[F841, RQW320]
|
help: Remove the rule code
91 | def f():
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
- # ruff: disable[F841, RQW320]
94 + # ruff: disable[F841]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
RUF102 [*] Invalid rule code in suppression: RQW320
--> suppressions.py:96:26
|
94 | # ruff: disable[F841, RQW320]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
| ^^^^^^
97 | # ruff: enable[YF829]
|
help: Remove the rule code
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
95 | value = 0
- # ruff: enable[F841, RQW320]
96 + # ruff: enable[F841]
97 | # ruff: enable[YF829]
98 |
99 |
RUF102 [*] Invalid rule code in suppression: YF829
--> suppressions.py:97:20
|
95 | value = 0
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
| ^^^^^
|
help: Remove the rule code
94 | # ruff: disable[F841, RQW320]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
- # ruff: enable[YF829]
97 |
98 |
99 | def f():
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
--> suppressions.py:109:5
|
107 | def f():
108 | # Empty or missing rule codes
109 | # ruff: disable
| ^^^^^^^^^^^^^^^
110 | # ruff: disable[]
111 | print("hello")
|
help: Remove suppression comment
106 |
107 | def f():
108 | # Empty or missing rule codes
- # ruff: disable
109 | # ruff: disable[]
110 | print("hello")
note: This is an unsafe fix and may change runtime behavior
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
--> suppressions.py:110:5
|
108 | # Empty or missing rule codes
109 | # ruff: disable
110 | # ruff: disable[]
| ^^^^^^^^^^^^^^^^^
111 | print("hello")
|
help: Remove suppression comment
107 | def f():
108 | # Empty or missing rule codes
109 | # ruff: disable
- # ruff: disable[]
110 | print("hello")
note: This is an unsafe fix and may change runtime behavior

View File

@@ -471,13 +471,6 @@ impl LinterSettings {
self
}
#[must_use]
pub fn with_external_rules(mut self, rules: &[&str]) -> Self {
self.external
.extend(rules.iter().map(std::string::ToString::to_string));
self
}
/// Resolve the [`TargetVersion`] to use for linting.
///
/// This method respects the per-file version overrides in

View File

@@ -4,7 +4,6 @@ use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation;
use rustc_hash::FxHashSet;
use std::cell::Cell;
use std::{error::Error, fmt::Formatter};
use thiserror::Error;
@@ -18,11 +17,7 @@ use crate::checkers::ast::LintContext;
use crate::codes::Rule;
use crate::fix::edits::delete_comment;
use crate::preview::is_range_suppressions_enabled;
use crate::rule_redirects::get_redirect_target;
use crate::rules::ruff::rules::{
InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind,
UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, UnusedNOQAKind, code_is_valid,
};
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
use crate::settings::LinterSettings;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -135,7 +130,7 @@ impl Suppressions {
}
pub(crate) fn is_empty(&self) -> bool {
self.valid.is_empty() && self.invalid.is_empty() && self.errors.is_empty()
self.valid.is_empty()
}
/// Check if a diagnostic is suppressed by any known range suppressions
@@ -155,9 +150,7 @@ impl Suppressions {
};
for suppression in &self.valid {
let suppression_code =
get_redirect_target(suppression.code.as_str()).unwrap_or(suppression.code.as_str());
if *code == suppression_code && suppression.range.contains_range(range) {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
suppression.used.set(true);
return true;
}
@@ -166,140 +159,81 @@ impl Suppressions {
}
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
let mut unmatched_ranges = FxHashSet::default();
for suppression in &self.valid {
if !code_is_valid(&suppression.code, &context.settings().external) {
// InvalidRuleCode
if context.is_rule_enabled(Rule::InvalidRuleCode) {
for comment in &suppression.comments {
let (range, edit) = Suppressions::delete_code_or_comment(
locator,
suppression,
comment,
true,
);
context
.report_diagnostic(
InvalidRuleCode {
rule_code: suppression.code.to_string(),
kind: InvalidRuleCodeKind::Suppression,
},
range,
)
.set_fix(Fix::safe_edit(edit));
}
}
} else if !suppression.used.get() {
// UnusedNOQA
if context.is_rule_enabled(Rule::UnusedNOQA) {
let Ok(rule) = Rule::from_code(
get_redirect_target(&suppression.code).unwrap_or(&suppression.code),
) else {
continue; // "external" lint code, don't treat it as unused
};
for comment in &suppression.comments {
let (range, edit) = Suppressions::delete_code_or_comment(
locator,
suppression,
comment,
false,
);
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
context
.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
)
.set_fix(Fix::safe_edit(edit));
}
}
} else if suppression.comments.len() == 1 {
// UnmatchedSuppressionComment
let range = suppression.comments[0].range;
if unmatched_ranges.insert(range) {
context.report_diagnostic_if_enabled(UnmatchedSuppressionComment {}, range);
}
}
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
return;
}
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
for error in &self.errors {
context
.report_diagnostic(
InvalidSuppressionComment {
kind: InvalidSuppressionCommentKind::Error(error.kind),
},
error.range,
)
.set_fix(Fix::unsafe_edit(delete_comment(error.range, locator)));
}
}
let unused = self
.valid
.iter()
.filter(|suppression| !suppression.used.get());
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
for invalid in &self.invalid {
context
.report_diagnostic(
InvalidSuppressionComment {
kind: InvalidSuppressionCommentKind::Invalid(invalid.kind),
},
invalid.comment.range,
)
.set_fix(Fix::unsafe_edit(delete_comment(
invalid.comment.range,
locator,
)));
}
}
}
fn delete_code_or_comment(
locator: &Locator<'_>,
suppression: &Suppression,
comment: &SuppressionComment,
highlight_only_code: bool,
) -> (TextRange, Edit) {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
if highlight_only_code {
range = comment.codes[0];
}
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
for suppression in unused {
let Ok(rule) = Rule::from_code(&suppression.code) else {
continue; // TODO: invalid code
};
Edit::range_deletion(code_range)
};
(range, edit)
for comment in &suppression.comments {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
};
Edit::range_deletion(code_range)
};
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
for error in self
.errors
.iter()
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
{
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(UnusedCodes::default()),
kind: UnusedNOQAKind::Suppression,
},
error.range,
);
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
}
}
}
@@ -457,7 +391,7 @@ impl<'a> SuppressionsBuilder<'a> {
}
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
pub(crate) enum ParseErrorKind {
enum ParseErrorKind {
#[error("not a suppression comment")]
NotASuppression,
@@ -467,7 +401,7 @@ pub(crate) enum ParseErrorKind {
#[error("unknown ruff directive")]
UnknownAction,
#[error("missing suppression codes like `[E501, ...]`")]
#[error("missing suppression codes")]
MissingCodes,
#[error("missing closing bracket")]

View File

@@ -12,7 +12,6 @@ pub use python_version::*;
pub mod comparable;
pub mod docstrings;
mod expression;
pub mod find_node;
mod generated;
pub mod helpers;
pub mod identifier;

View File

@@ -2,25 +2,18 @@
use crate::{self as ast, AnyNodeRef, ExceptHandler, Stmt};
/// Given a [`Stmt`] and its parent, return the [`ast::Suite`] that contains the [`Stmt`].
pub fn suite<'a>(
stmt: impl Into<AnyNodeRef<'a>>,
parent: impl Into<AnyNodeRef<'a>>,
) -> Option<EnclosingSuite<'a>> {
pub fn suite<'a>(stmt: &'a Stmt, parent: &'a Stmt) -> Option<EnclosingSuite<'a>> {
// TODO: refactor this to work without a parent, ie when `stmt` is at the top level
let stmt = stmt.into();
match parent.into() {
AnyNodeRef::ModModule(ast::ModModule { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) => {
EnclosingSuite::new(body, stmt)
}
AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) => [body, orelse]
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::ClassDef(ast::StmtClassDef { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::For(ast::StmtFor { body, orelse, .. }) => [body, orelse]
.iter()
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
Stmt::While(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
.iter()
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtIf(ast::StmtIf {
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
@@ -28,12 +21,12 @@ pub fn suite<'a>(
.into_iter()
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => cases
Stmt::With(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::Match(ast::StmtMatch { cases, .. }) => cases
.iter()
.map(|case| &case.body)
.find_map(|body| EnclosingSuite::new(body, stmt)),
AnyNodeRef::StmtTry(ast::StmtTry {
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
@@ -58,10 +51,10 @@ pub struct EnclosingSuite<'a> {
}
impl<'a> EnclosingSuite<'a> {
pub fn new(suite: &'a [Stmt], stmt: AnyNodeRef<'a>) -> Option<Self> {
pub fn new(suite: &'a [Stmt], stmt: &'a Stmt) -> Option<Self> {
let position = suite
.iter()
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt))?;
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt.into()))?;
Some(EnclosingSuite { suite, position })
}

View File

@@ -222,17 +222,6 @@ where
visitor.leave_node(node);
}
pub fn walk_node<'a, V>(visitor: &mut V, node: AnyNodeRef<'a>)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
if visitor.enter_node(node).is_traverse() {
node.visit_source_order(visitor);
}
visitor.leave_node(node);
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum TraversalSignal {
Traverse,

View File

@@ -243,7 +243,7 @@ fn to_lsp_diagnostic(
) -> (usize, lsp_types::Diagnostic) {
let diagnostic_range = diagnostic.range().unwrap_or_default();
let name = diagnostic.name();
let body = diagnostic.concise_message().to_string();
let body = diagnostic.body().to_string();
let fix = diagnostic.fix();
let suggestion = diagnostic.first_help_text();
let code = diagnostic.secondary_code();

View File

@@ -179,45 +179,42 @@ impl LineIndex {
let line = self.line_index(offset);
let line_start = self.line_start(line, text);
let character_offset =
self.characters_between(TextRange::new(line_start, offset), text, encoding);
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character_offset),
}
}
fn characters_between(
&self,
range: TextRange,
text: &str,
encoding: PositionEncoding,
) -> usize {
if self.is_ascii() {
return (range.end() - range.start()).to_usize();
return SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed((offset - line_start).to_usize()),
};
}
match encoding {
PositionEncoding::Utf8 => (range.end() - range.start()).to_usize(),
PositionEncoding::Utf8 => {
let character_offset = offset - line_start;
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character_offset.to_usize()),
}
}
PositionEncoding::Utf16 => {
let up_to_character = &text[range];
up_to_character.encode_utf16().count()
let up_to_character = &text[TextRange::new(line_start, offset)];
let character = up_to_character.encode_utf16().count();
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character),
}
}
PositionEncoding::Utf32 => {
let up_to_character = &text[range];
up_to_character.chars().count()
let up_to_character = &text[TextRange::new(line_start, offset)];
let character = up_to_character.chars().count();
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character),
}
}
}
}
/// Returns the length of the line in characters, respecting the given encoding
pub fn line_len(&self, line: OneIndexed, text: &str, encoding: PositionEncoding) -> usize {
let line_range = self.line_range(line, text);
self.characters_between(line_range, text, encoding)
}
/// Return the number of lines in the source code.
pub fn line_count(&self) -> usize {
self.line_starts().len()

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.10"
version = "0.14.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -241,7 +241,7 @@ impl Workspace {
let range = msg.range().unwrap_or_default();
ExpandedMessage {
code: msg.secondary_code_or_id().to_string(),
message: msg.concise_message().to_string(),
message: msg.body().to_string(),
start_location: source_code
.source_location(range.start(), self.position_encoding)
.into(),

View File

@@ -42,7 +42,6 @@ wild = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
dunce = { workspace = true }
insta = { workspace = true, features = ["filters"] }

1
crates/ty/docs/cli.md generated
View File

@@ -56,7 +56,6 @@ over all configuration files.</p>
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times).</p>
<p>This is an advanced option that should usually only be used for first-party or third-party modules that are not installed into your Python environment in a conventional way. Use <code>--python</code> to point ty to your Python environment if it is in an unusual location.</p>
</dd><dt id="ty-check--force-exclude"><a href="#ty-check--force-exclude"><code>--force-exclude</code></a></dt><dd><p>Enforce exclusions, even for paths passed to ty directly on the command-line. Use <code>--no-force-exclude</code> to disable</p>
</dd><dt id="ty-check--help"><a href="#ty-check--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help (see a summary with '-h')</p>
</dd><dt id="ty-check--ignore"><a href="#ty-check--ignore"><code>--ignore</code></a> <i>rule</i></dt><dd><p>Disables the rule. Can be specified multiple times.</p>
</dd><dt id="ty-check--no-progress"><a href="#ty-check--no-progress"><code>--no-progress</code></a></dt><dd><p>Hide all progress outputs.</p>

View File

@@ -20,59 +20,11 @@ Valid severities are:
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
=== "ty.toml"
```toml
[rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
---
## `analysis`
### `respect-type-ignore-comments`
Whether ty should respect `type: ignore` comments.
When set to `false`, `type: ignore` comments are treated like any other normal
comment and can't be used to suppress ty errors (you have to use `ty: ignore` instead).
Setting this option can be useful when using ty alongside other type checkers or when
you prefer using `ty: ignore` over `type: ignore`.
Defaults to `true`.
**Default value**: `true`
**Type**: `bool`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.analysis]
# Disable support for `type: ignore` comments
respect-type-ignore-comments = false
```
=== "ty.toml"
```toml
[analysis]
# Disable support for `type: ignore` comments
respect-type-ignore-comments = false
```
```toml title="pyproject.toml"
[tool.ty.rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
---
@@ -95,19 +47,10 @@ configuration setting.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
extra-paths = ["./shared/my-search-path"]
```
=== "ty.toml"
```toml
[environment]
extra-paths = ["./shared/my-search-path"]
```
```toml title="pyproject.toml"
[tool.ty.environment]
extra-paths = ["./shared/my-search-path"]
```
---
@@ -135,19 +78,10 @@ This option can be used to point to virtual or system Python environments.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
python = "./custom-venv-location/.venv"
```
=== "ty.toml"
```toml
[environment]
python = "./custom-venv-location/.venv"
```
```toml title="pyproject.toml"
[tool.ty.environment]
python = "./custom-venv-location/.venv"
```
---
@@ -171,21 +105,11 @@ If no platform is specified, ty will use the current platform:
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
=== "ty.toml"
```toml
[environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
```toml title="pyproject.toml"
[tool.ty.environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
---
@@ -215,19 +139,10 @@ to reflect the differing contents of the standard library across Python versions
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
python-version = "3.12"
```
=== "ty.toml"
```toml
[environment]
python-version = "3.12"
```
```toml title="pyproject.toml"
[tool.ty.environment]
python-version = "3.12"
```
---
@@ -252,21 +167,11 @@ it will also be included in the first party search path.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
=== "ty.toml"
```toml
[environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
```toml title="pyproject.toml"
[tool.ty.environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
---
@@ -282,19 +187,10 @@ bundled as a zip file in the binary
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.environment]
typeshed = "/path/to/custom/typeshed"
```
=== "ty.toml"
```toml
[environment]
typeshed = "/path/to/custom/typeshed"
```
```toml title="pyproject.toml"
[tool.ty.environment]
typeshed = "/path/to/custom/typeshed"
```
---
@@ -344,29 +240,15 @@ If not specified, defaults to `[]` (excludes no files).
**Example usage**:
=== "pyproject.toml"
```toml
[[tool.ty.overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "ty.toml"
```toml
[[overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
```toml title="pyproject.toml"
[[tool.ty.overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
---
@@ -386,25 +268,13 @@ If not specified, defaults to `["**"]` (matches all files).
**Example usage**:
=== "pyproject.toml"
```toml
[[tool.ty.overrides]]
include = [
"src",
"tests",
]
```
=== "ty.toml"
```toml
[[overrides]]
include = [
"src",
"tests",
]
```
```toml title="pyproject.toml"
[[tool.ty.overrides]]
include = [
"src",
"tests",
]
```
---
@@ -422,25 +292,13 @@ severity levels or disable them entirely.
**Example usage**:
=== "pyproject.toml"
```toml title="pyproject.toml"
[[tool.ty.overrides]]
include = ["src"]
```toml
[[tool.ty.overrides]]
include = ["src"]
[tool.ty.overrides.rules]
possibly-unresolved-reference = "ignore"
```
=== "ty.toml"
```toml
[[overrides]]
include = ["src"]
[overrides.rules]
possibly-unresolved-reference = "ignore"
```
[tool.ty.overrides.rules]
possibly-unresolved-reference = "ignore"
```
---
@@ -500,29 +358,15 @@ to re-include `dist` use `exclude = ["!dist"]`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "ty.toml"
```toml
[src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
```toml title="pyproject.toml"
[tool.ty.src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
---
@@ -555,25 +399,13 @@ matches `<project_root>/src` and not `<project_root>/test/src`).
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.src]
include = [
"src",
"tests",
]
```
=== "ty.toml"
```toml
[src]
include = [
"src",
"tests",
]
```
```toml title="pyproject.toml"
[tool.ty.src]
include = [
"src",
"tests",
]
```
---
@@ -589,19 +421,10 @@ Enabled by default.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.src]
respect-ignore-files = false
```
=== "ty.toml"
```toml
[src]
respect-ignore-files = false
```
```toml title="pyproject.toml"
[tool.ty.src]
respect-ignore-files = false
```
---
@@ -627,19 +450,10 @@ it will also be included in the first party search path.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.src]
root = "./app"
```
=== "ty.toml"
```toml
[src]
root = "./app"
```
```toml title="pyproject.toml"
[tool.ty.src]
root = "./app"
```
---
@@ -657,21 +471,11 @@ Defaults to `false`.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
=== "ty.toml"
```toml
[terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
```toml title="pyproject.toml"
[tool.ty.terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
---
@@ -687,19 +491,10 @@ Defaults to `full`.
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.terminal]
output-format = "concise"
```
=== "ty.toml"
```toml
[terminal]
output-format = "concise"
```
```toml title="pyproject.toml"
[tool.ty.terminal]
output-format = "concise"
```
---

1327
crates/ty/docs/rules.md generated

File diff suppressed because it is too large Load Diff

View File

@@ -154,17 +154,6 @@ pub(crate) struct CheckCommand {
#[clap(long, overrides_with("respect_ignore_files"), hide = true)]
no_respect_ignore_files: bool,
/// Enforce exclusions, even for paths passed to ty directly on the command-line.
/// Use `--no-force-exclude` to disable.
#[arg(
long,
overrides_with("no_force_exclude"),
help_heading = "File selection"
)]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Glob patterns for files to exclude from type checking.
///
/// Uses gitignore-style syntax to exclude files and directories from type checking.
@@ -189,10 +178,6 @@ pub(crate) struct CheckCommand {
}
impl CheckCommand {
pub(crate) fn force_exclude(&self) -> bool {
resolve_bool_arg(self.force_exclude, self.no_force_exclude).unwrap_or_default()
}
pub(crate) fn into_options(self) -> Options {
let rules = if self.rules.is_empty() {
None
@@ -450,12 +435,3 @@ impl ConfigsArg {
self.0
}
}
fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
match (yes, no) {
(true, false) => Some(true),
(false, true) => Some(false),
(false, false) => None,
(..) => unreachable!("Clap should make this impossible"),
}
}

View File

@@ -10,9 +10,9 @@ use ty_static::EnvVars;
use std::fmt::Write;
use std::process::{ExitCode, Termination};
use std::sync::Mutex;
use anyhow::Result;
use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::{VerbosityLevel, setup_tracing};
@@ -22,7 +22,6 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::cancellation::{CancellationToken, CancellationTokenSource};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
};
@@ -119,12 +118,9 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
.config_file
.as_ref()
.map(|path| SystemPath::absolute(path, &cwd));
let force_exclude = args.force_exclude();
let mut project_metadata = match &config_file {
Some(config_file) => {
ProjectMetadata::from_config_file(config_file.clone(), &project_path, &system)?
}
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
None => ProjectMetadata::discover(&project_path, &system)?,
};
@@ -134,13 +130,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
project_metadata.apply_overrides(&project_options_overrides);
let mut db = ProjectDatabase::new(project_metadata, system)?;
let project = db.project();
project.set_verbose(&mut db, verbosity >= VerbosityLevel::Verbose);
project.set_force_exclude(&mut db, force_exclude);
db.project()
.set_verbose(&mut db, verbosity >= VerbosityLevel::Verbose);
if !check_paths.is_empty() {
project.set_included_paths(&mut db, check_paths);
db.project().set_included_paths(&mut db, check_paths);
}
let (main_loop, main_loop_cancellation_token) =
@@ -228,11 +222,6 @@ struct MainLoop {
printer: Printer,
project_options_overrides: ProjectOptionsOverrides,
/// Cancellation token that gets set by Ctrl+C.
/// Used for long-running operations on the main thread. Operations on background threads
/// use Salsa's cancellation mechanism.
cancellation_token: CancellationToken,
}
impl MainLoop {
@@ -242,9 +231,6 @@ impl MainLoop {
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
let cancellation_token_source = CancellationTokenSource::new();
let cancellation_token = cancellation_token_source.token();
(
Self {
sender: sender.clone(),
@@ -252,12 +238,8 @@ impl MainLoop {
watcher: None,
project_options_overrides,
printer,
cancellation_token,
},
MainLoopCancellationToken {
sender,
source: cancellation_token_source,
},
MainLoopCancellationToken { sender },
)
}
@@ -291,6 +273,9 @@ impl MainLoop {
let mut revision = 0u64;
while let Ok(message) = self.receiver.recv() {
if self.watcher.is_some() {
Printer::clear_screen()?;
}
match message {
MainLoopMessage::CheckWorkspace => {
let db = db.clone();
@@ -329,7 +314,6 @@ impl MainLoop {
let display_config = DisplayDiagnosticConfig::default()
.format(terminal_settings.output_format.into())
.color(colored::control::SHOULD_COLORIZE.should_colorize())
.with_cancellation_token(Some(self.cancellation_token.clone()))
.show_fix_diff(true);
if check_revision == revision {
@@ -373,21 +357,19 @@ impl MainLoop {
)?;
}
if !self.cancellation_token.is_cancelled() {
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if exit_status.is_internal_error() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
}
if exit_status.is_internal_error() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
}
if self.watcher.is_none() {
@@ -402,15 +384,12 @@ impl MainLoop {
}
MainLoopMessage::ApplyChanges(changes) => {
Printer::clear_screen()?;
revision += 1;
// Automatically cancels any pending queries and waits for them to complete.
db.apply_changes(changes, Some(&self.project_options_overrides));
if let Some(watcher) = self.watcher.as_mut() {
watcher.update(db);
}
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
}
MainLoopMessage::Exit => {
@@ -514,12 +493,10 @@ impl ty_project::ProgressReporter for IndicatifReporter {
#[derive(Debug)]
struct MainLoopCancellationToken {
sender: crossbeam_channel::Sender<MainLoopMessage>,
source: CancellationTokenSource,
}
impl MainLoopCancellationToken {
fn stop(self) {
self.source.cancel();
self.sender.send(MainLoopMessage::Exit).unwrap();
}
}

View File

@@ -1,43 +0,0 @@
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
/// ty ignores `type: ignore` comments when setting `respect-type-ignore-comments=false`
#[test]
fn respect_type_ignore_comments_is_turned_off() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
y = a + 5 # type: ignore
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
assert_cmd_snapshot!(case.command().arg("--config").arg("analysis.respect-type-ignore-comments=false"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `a` used when not defined
--> test.py:2:5
|
2 | y = a + 5 # type: ignore
| ^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
");
Ok(())
}

View File

@@ -133,7 +133,7 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
1 | bad-option=true
| ^^^^^^^^^^
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `analysis`, `overrides`
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `overrides`
Usage: ty <COMMAND>

View File

@@ -589,128 +589,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> {
Ok(())
}
/// Test behavior when explicitly checking a path that matches an exclude pattern and `--force-exclude` is provided
#[test]
fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"src/main.py",
r#"
print(undefined_var) # error: unresolved-reference
"#,
),
(
"tests/generated.py",
r#"
print(dist_undefined_var) # error: unresolved-reference
"#,
),
(
"dist/other.py",
r#"
print(other_undefined_var) # error: unresolved-reference
"#,
),
(
"ty.toml",
r#"
[src]
exclude = ["tests/generated.py"]
"#,
),
])?;
// Explicitly checking a file in an excluded directory should still check that file
assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> src/main.py:2:7
|
2 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `dist_undefined_var` used when not defined
--> tests/generated.py:2:7
|
2 | print(dist_undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
");
// Except when `--force-exclude` is set.
assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py").arg("--force-exclude"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> src/main.py:2:7
|
2 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
");
// Explicitly checking the entire excluded directory should check all files in it
assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `other_undefined_var` used when not defined
--> dist/other.py:2:7
|
2 | print(other_undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `undefined_var` used when not defined
--> src/main.py:2:7
|
2 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
");
// Except when using `--force-exclude`
assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py").arg("--force-exclude"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> src/main.py:2:7
|
2 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn cli_and_configuration_exclude() -> anyhow::Result<()> {
let case = CliTest::with_files([

View File

@@ -1,4 +1,3 @@
mod analysis_options;
mod config_option;
mod exit_code;
mod file_selection;
@@ -659,8 +658,6 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
from typing_extensions import reveal_type
reveal_type('str'.lower()) # [revealed-type]
"#,
)?;
@@ -711,25 +708,6 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
}
}
}
},
{
"check_name": "revealed-type",
"description": "revealed-type: Revealed type: `LiteralString`",
"severity": "info",
"fingerprint": "[FINGERPRINT]",
"location": {
"path": "test.py",
"positions": {
"begin": {
"line": 5,
"column": 13
},
"end": {
"line": 5,
"column": 26
}
}
}
}
]
----- stderr -----
@@ -745,8 +723,6 @@ fn github_diagnostics() -> anyhow::Result<()> {
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
from typing_extensions import reveal_type
reveal_type('str'.lower()) # [revealed-type]
"#,
)?;
@@ -756,7 +732,6 @@ fn github_diagnostics() -> anyhow::Result<()> {
----- stdout -----
::warning title=ty (unresolved-reference),file=<temp_dir>/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
::notice title=ty (revealed-type),file=<temp_dir>/test.py,line=5,col=13,endLine=5,endColumn=26::test.py:5:13: revealed-type: Revealed type: `LiteralString`
----- stderr -----
");

View File

@@ -2703,51 +2703,3 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
Ok(())
}
/// Test behavior when `VIRTUAL_ENV` is set but points to a non-existent path.
#[test]
fn missing_virtual_env() -> anyhow::Result<()> {
let working_venv_package1_path = if cfg!(windows) {
"project/.venv/Lib/site-packages/package1/__init__.py"
} else {
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
from package1 import WorkingVenv
"#,
),
(
"project/.venv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
working_venv_package1_path,
r#"
class WorkingVenv: ...
"#,
),
])?;
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("nonexistent-venv")), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Failed to discover local Python environment
Cause: Invalid `VIRTUAL_ENV` environment variable `<temp_dir>/nonexistent-venv`: does not point to a directory on disk
Cause: No such file or directory (os error 2)
");
Ok(())
}

View File

@@ -10,13 +10,12 @@ use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard, file_time_now,
};
use ruff_python_ast::PythonVersion;
use ty_module_resolver::{Module, ModuleName, resolve_module_confident};
use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides};
use ty_project::metadata::pyproject::{PyProject, Tool};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_python_semantic::PythonPlatform;
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_confident};
struct TestCase {
db: ProjectDatabase,
@@ -1020,7 +1019,7 @@ fn search_path() -> anyhow::Result<()> {
let site_packages = case.root_path().join("site_packages");
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new("a").unwrap()),
None
);
@@ -1193,7 +1192,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
// Unset the custom typeshed directory.
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new_static("os").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()),
None
);
@@ -1208,7 +1207,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("os").unwrap()).is_some());
assert!(resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()).is_some());
Ok(())
}
@@ -1875,11 +1874,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
let mut case = setup([("lib.py", "class Foo: ...")])?;
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("lib").unwrap()).is_some(),
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
"Expected `lib` module to exist."
);
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new_static("Lib").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()),
None,
"Expected `Lib` module not to exist"
);
@@ -1912,13 +1911,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
// Resolving `lib` should now fail but `Lib` should now succeed
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new_static("lib").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()),
None,
"Expected `lib` module to no longer exist."
);
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("Lib").unwrap()).is_some(),
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
"Expected `Lib` module to exist"
);

View File

@@ -12,6 +12,7 @@ license.workspace = true
[dependencies]
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ty_python_semantic = { workspace = true }
ordermap = { workspace = true }

View File

@@ -8,6 +8,7 @@ use std::{collections::HashMap, hash::BuildHasher};
use ordermap::OrderMap;
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
use ty_python_semantic::PythonPlatform;
/// Combine two values, preferring the values in `self`.
///
@@ -144,6 +145,7 @@ macro_rules! impl_noop_combine {
}
impl_noop_combine!(SystemPathBuf);
impl_noop_combine!(PythonPlatform);
impl_noop_combine!(PythonVersion);
// std types

View File

@@ -15,8 +15,8 @@ ruff_db = { workspace = true, features = ["os"] }
ruff_text_size = { workspace = true }
ty_ide = { workspace = true }
ty_module_resolver = { workspace = true }
ty_project = { workspace = true }
ty_python_semantic = { workspace = true }
anyhow = { workspace = true }
bstr = { workspace = true }

View File

@@ -15,7 +15,7 @@ import-deprioritizes-type_check_only,main.py,3,2
import-deprioritizes-type_check_only,main.py,4,3
import-keyword-completion,main.py,0,1
internal-typeshed-hidden,main.py,0,2
none-completion,main.py,0,1
none-completion,main.py,0,2
numpy-array,main.py,0,159
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
@@ -28,4 +28,4 @@ scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,9
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,253
type-var-typing-over-ast,main.py,1,251
1 name file index rank
15 import-deprioritizes-type_check_only main.py 4 3
16 import-keyword-completion main.py 0 1
17 internal-typeshed-hidden main.py 0 2
18 none-completion main.py 0 1 2
19 numpy-array main.py 0 159
20 numpy-array main.py 1 1
21 object-attr-instance-methods main.py 0 1
28 tstring-completions main.py 0 1
29 ty-extensions-lower-stdlib main.py 0 9
30 type-var-typing-over-ast main.py 0 3
31 type-var-typing-over-ast main.py 1 253 251

View File

@@ -15,11 +15,11 @@ use regex::bytes::Regex;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ty_ide::Completion;
use ty_module_resolver::ModuleName;
use ty_project::metadata::Options;
use ty_project::metadata::options::EnvironmentOptions;
use ty_project::metadata::value::RelativePathBuf;
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_python_semantic::ModuleName;
#[derive(Debug, clap::Parser)]
#[command(

View File

@@ -22,9 +22,7 @@ ruff_python_importer = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true }
ty_python_types = { workspace = true }
ty_project = { workspace = true, features = ["testing"] }
ty_vendored = { workspace = true }

View File

@@ -1,6 +1,6 @@
use ruff_db::files::File;
use ty_module_resolver::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use ty_project::Db;
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use crate::{
SymbolKind,
@@ -21,7 +21,7 @@ pub fn all_symbols<'db>(
return Vec::new();
}
let typing_extensions = ModuleName::new_static("typing_extensions").unwrap();
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
let is_typing_extensions_available = importing_from.is_stub(db)
|| resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some();

View File

@@ -1,13 +1,12 @@
use crate::completion;
use crate::{completion, find_node::covering_node};
use ruff_db::{files::File, parsed::parsed_module};
use ruff_diagnostics::Edit;
use ruff_python_ast::find_node::covering_node;
use ruff_text_size::TextRange;
use ty_project::Db;
use ty_python_semantic::create_suppression_fix;
use ty_python_semantic::lint::LintId;
use ty_python_types::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
use ty_python_semantic::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
/// A `QuickFix` Code Action
#[derive(Debug, Clone)]
@@ -87,8 +86,10 @@ mod tests {
use ruff_python_trivia::textwrap::dedent;
use ruff_text_size::{TextRange, TextSize};
use ty_project::ProjectMetadata;
use ty_python_semantic::lint::LintMetadata;
use ty_python_types::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
use ty_python_semantic::{
lint::LintMetadata,
types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE},
};
#[test]
fn add_ignore() {

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget};
use ruff_db::files::File;
use ruff_text_size::TextSize;
use ty_python_types::SemanticModel;
use ty_python_semantic::SemanticModel;
/// Find all document highlights for a symbol at the given position.
/// Document highlights are limited to the current file only.

View File

@@ -1,5 +1,5 @@
use crate::AnyNodeRef;
use crate::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_node};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
use ruff_text_size::{Ranged, TextRange};
use std::fmt;
use std::fmt::Formatter;
@@ -11,7 +11,7 @@ use std::fmt::Formatter;
///
/// ## Panics
/// Panics if `range` is not contained within `root`.
pub fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
struct Visitor<'a> {
range: TextRange,
found: bool,
@@ -48,12 +48,15 @@ pub fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
ancestors: Vec::new(),
};
walk_node(&mut visitor, root);
root.visit_source_order(&mut visitor);
if visitor.ancestors.is_empty() {
visitor.ancestors.push(root);
}
CoveringNode::from_ancestors(visitor.ancestors)
}
/// The node with a minimal range that fully contains the search range.
pub struct CoveringNode<'a> {
pub(crate) struct CoveringNode<'a> {
/// The covering node, along with all of its ancestors up to the
/// root. The root is always the first element and the covering
/// node found is always the last node. This sequence is guaranteed
@@ -64,12 +67,12 @@ pub struct CoveringNode<'a> {
impl<'a> CoveringNode<'a> {
/// Creates a new `CoveringNode` from a list of ancestor nodes.
/// The ancestors should be ordered from root to the covering node.
pub fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
pub(crate) fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
Self { nodes: ancestors }
}
/// Returns the covering node found.
pub fn node(&self) -> AnyNodeRef<'a> {
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
*self
.nodes
.last()
@@ -77,7 +80,7 @@ impl<'a> CoveringNode<'a> {
}
/// Returns the node's parent.
pub fn parent(&self) -> Option<AnyNodeRef<'a>> {
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
let penultimate = self.nodes.len().checked_sub(2)?;
self.nodes.get(penultimate).copied()
}
@@ -87,7 +90,7 @@ impl<'a> CoveringNode<'a> {
///
/// The "first" here means that the node closest to a leaf is
/// returned.
pub fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
pub(crate) fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
let Some(index) = self.find_first_index(f) else {
return Err(self);
};
@@ -102,7 +105,7 @@ impl<'a> CoveringNode<'a> {
/// the highest ancestor found satisfying the given predicate is
/// returned. Note that this is *not* the same as finding the node
/// closest to the root that satisfies the given predictate.
pub fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
pub(crate) fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
let Some(mut index) = self.find_first_index(&f) else {
return Err(self);
};
@@ -115,7 +118,7 @@ impl<'a> CoveringNode<'a> {
/// Returns an iterator over the ancestor nodes, starting with the node itself
/// and walking towards the root.
pub fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
pub(crate) fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
self.nodes.iter().copied().rev()
}

View File

@@ -3,7 +3,7 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget};
use ruff_db::files::File;
use ruff_text_size::TextSize;
use ty_python_types::SemanticModel;
use ty_python_semantic::SemanticModel;
/// Find all references to a symbol at the given position.
/// Search for references across all files in the project.

View File

@@ -5,19 +5,19 @@ pub use crate::goto_type_definition::goto_type_definition;
use std::borrow::Cow;
use crate::find_node::covering_node;
use crate::stub_mapping::StubMapper;
use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::find_node::{CoveringNode, covering_node};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_types::ResolvedDefinition;
use ty_python_types::types::Type;
use ty_python_types::types::ide_support::{
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::{
call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument,
};
use ty_python_types::{
use ty_python_semantic::{
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
definitions_for_name,
};
@@ -228,15 +228,15 @@ impl<'db> Definitions<'db> {
pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option<Self> {
let ty_def = ty.definition(db)?;
let resolved = match ty_def {
ty_python_types::types::TypeDefinition::Module(module) => {
ty_python_semantic::types::TypeDefinition::Module(module) => {
ResolvedDefinition::Module(module.file(db)?)
}
ty_python_types::types::TypeDefinition::Class(definition)
| ty_python_types::types::TypeDefinition::Function(definition)
| ty_python_types::types::TypeDefinition::TypeVar(definition)
| ty_python_types::types::TypeDefinition::TypeAlias(definition)
| ty_python_types::types::TypeDefinition::SpecialForm(definition)
| ty_python_types::types::TypeDefinition::NewType(definition) => {
ty_python_semantic::types::TypeDefinition::Class(definition)
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
| ty_python_semantic::types::TypeDefinition::SpecialForm(definition)
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
ResolvedDefinition::Definition(definition)
}
};
@@ -338,11 +338,11 @@ impl GotoTarget<'_> {
}
}
GotoTarget::BinOp { expression, .. } => {
let (_, ty) = ty_python_types::definitions_for_bin_op(model, expression)?;
let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?;
Some(ty)
}
GotoTarget::UnaryOp { expression, .. } => {
let (_, ty) = ty_python_types::definitions_for_unary_op(model, expression)?;
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
Some(ty)
}
// TODO: Support identifier targets
@@ -526,14 +526,15 @@ impl GotoTarget<'_> {
}
GotoTarget::BinOp { expression, .. } => {
let (definitions, _) = ty_python_types::definitions_for_bin_op(model, expression)?;
let (definitions, _) =
ty_python_semantic::definitions_for_bin_op(model, expression)?;
Some(definitions)
}
GotoTarget::UnaryOp { expression, .. } => {
let (definitions, _) =
ty_python_types::definitions_for_unary_op(model, expression)?;
ty_python_semantic::definitions_for_unary_op(model, expression)?;
Some(definitions)
}
@@ -664,7 +665,7 @@ impl GotoTarget<'_> {
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
pub(crate) fn from_covering_node<'a>(
model: &SemanticModel,
covering_node: &CoveringNode<'a>,
covering_node: &crate::find_node::CoveringNode<'a>,
offset: TextSize,
tokens: &Tokens,
) -> Option<GotoTarget<'a>> {
@@ -938,12 +939,12 @@ impl Ranged for GotoTarget<'_> {
/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items.
fn convert_resolved_definitions_to_targets<'db>(
db: &'db dyn ty_python_semantic::Db,
definitions: Vec<ty_python_types::ResolvedDefinition<'db>>,
definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
) -> Vec<crate::NavigationTarget> {
definitions
.into_iter()
.map(|resolved_definition| match resolved_definition {
ty_python_types::ResolvedDefinition::Definition(definition) => {
ty_python_semantic::ResolvedDefinition::Definition(definition) => {
// Get the parsed module for range calculation
let definition_file = definition.file(db);
let module = ruff_db::parsed::parsed_module(db, definition_file).load(db);
@@ -958,11 +959,11 @@ fn convert_resolved_definitions_to_targets<'db>(
full_range: full_range.range(),
}
}
ty_python_types::ResolvedDefinition::Module(file) => {
ty_python_semantic::ResolvedDefinition::Module(file) => {
// For modules, navigate to the start of the file
crate::NavigationTarget::new(file, TextRange::default())
}
ty_python_types::ResolvedDefinition::FileWithRange(file_range) => {
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
// For file ranges, navigate to the specific range within the file
crate::NavigationTarget::from(file_range)
}
@@ -983,9 +984,9 @@ fn definitions_for_expression<'db>(
expression.into(),
alias_resolution,
)),
ast::ExprRef::Attribute(attribute) => {
Some(ty_python_types::definitions_for_attribute(model, attribute))
}
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
model, attribute,
)),
_ => None,
}
}
@@ -1006,7 +1007,7 @@ fn definitions_for_callable<'db>(
fn definitions_to_navigation_targets<'db>(
db: &dyn ty_python_semantic::Db,
stub_mapper: Option<&StubMapper<'db>>,
mut definitions: Vec<ty_python_types::ResolvedDefinition<'db>>,
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
) -> Option<crate::NavigationTargets> {
if let Some(mapper) = stub_mapper {
definitions = mapper.map_definitions(definitions);

View File

@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_types::{ImportAliasResolution, SemanticModel};
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the declaration of a symbol.
///
@@ -386,29 +386,6 @@ FOO = 0
");
}
#[test]
fn goto_declaration_from_import_rhs_is_module() {
let test = CursorTest::builder()
.source("lib/__init__.py", r#""#)
.source("lib/module.py", r#""#)
.source("main.py", r#"from lib import module<CURSOR>"#)
.build();
// Should resolve to the actual function definition, not the import statement
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Go to declaration
--> main.py:1:17
|
1 | from lib import module
| ^^^^^^ Clicking here
|
info: Found 1 declaration
--> lib/module.py:1:1
|
|
");
}
#[test]
fn goto_declaration_from_import_as() {
let test = CursorTest::builder()

View File

@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_types::{ImportAliasResolution, SemanticModel};
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the definition of a symbol.
///

View File

@@ -3,7 +3,7 @@ use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_types::SemanticModel;
use ty_python_semantic::SemanticModel;
pub fn goto_type_definition(
db: &dyn Db,
@@ -1339,13 +1339,13 @@ f(**kwargs<CURSOR>)
| ^^^^^^ Clicking here
|
info: Found 1 type definition
--> stdlib/builtins.pyi:2947:7
--> stdlib/builtins.pyi:2920:7
|
2946 | @disjoint_base
2947 | class dict(MutableMapping[_KT, _VT]):
2919 | @disjoint_base
2920 | class dict(MutableMapping[_KT, _VT]):
| ----
2948 | """dict() -> new empty dictionary
2949 | dict(mapping) -> new dictionary initialized from a mapping object's
2921 | """dict() -> new empty dictionary
2922 | dict(mapping) -> new dictionary initialized from a mapping object's
|
"#);
}

View File

@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use std::fmt;
use std::fmt::Formatter;
use ty_python_types::types::{KnownInstanceType, Type, TypeVarVariance};
use ty_python_types::{DisplaySettings, SemanticModel};
use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance};
use ty_python_semantic::{DisplaySettings, SemanticModel};
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
let parsed = parsed_module(db, file).load(db);
@@ -23,7 +23,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
let docs = goto_target
.get_definition_targets(
&model,
ty_python_types::ImportAliasResolution::ResolveAliases,
ty_python_semantic::ImportAliasResolution::ResolveAliases,
)
.and_then(|definitions| definitions.docstring(db))
.map(HoverContent::Docstring);
@@ -187,42 +187,29 @@ mod tests {
let test = cursor_test(
r#"
a = 10
"""This is the docs for this value
Wow these are good docs!
"""
a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[10]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[10]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:8:1
--> main.py:4:1
|
6 | """
7 |
8 | a
2 | a = 10
3 |
4 | a
| ^- Cursor offset
| |
| source
|
"#);
");
}
#[test]
@@ -716,10 +703,6 @@ mod tests {
def __init__(a: int, b: str):
self.a = a
"""This is the docs for this value
Wow these are good docs!
"""
self.b: str = b
foo = Foo()
@@ -735,10 +718,10 @@ mod tests {
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:14:5
--> main.py:10:5
|
13 | foo = Foo()
14 | foo.a
9 | foo = Foo()
10 | foo.a
| -
| |
| source
@@ -2362,28 +2345,15 @@ def function():
let test = cursor_test(
r#"
value<CURSOR> = 1
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:1
@@ -2392,9 +2362,8 @@ def function():
| ^^^^^- Cursor offset
| |
| source
3 | """This is the docs for this value
|
"#);
");
}
#[test]
@@ -2402,15 +2371,7 @@ def function():
let test = cursor_test(
r#"
value = 1
"""This is the docs for this value
Wow these are good docs!
"""
value<CURSOR> += 2
"""Other docs???
Is this allowed???
"""
"#,
);
@@ -2418,34 +2379,23 @@ def function():
// Showing the new value might be more intuitive for some users, but the actual 'use'
// of the `value` symbol here in read-context is `1`. This comment mainly exists to
// signal that it might be okay to revisit this in the future and reveal 3 instead.
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:7:1
--> main.py:3:1
|
5 | Wow these are good docs!
6 | """
7 | value += 2
2 | value = 1
3 | value += 2
| ^^^^^- Cursor offset
| |
| source
8 | """Other docs???
|
"#);
");
}
#[test]
@@ -2454,47 +2404,29 @@ def function():
r#"
class C:
attr: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
C.attr<CURSOR> = 2
"""Other docs???
Is this allowed???
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[2]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[2]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:9:3
|
7 | """
8 |
9 | C.attr = 2
| ^^^^- Cursor offset
| |
| source
10 | """Other docs???
|
"#);
--> main.py:5:3
|
3 | attr: int = 1
4 |
5 | C.attr = 2
| ^^^^- Cursor offset
| |
| source
|
");
}
#[test]
@@ -2503,49 +2435,31 @@ def function():
r#"
class C:
attr = 1
"""This is the docs for this value
Wow these are good docs!
"""
C.attr<CURSOR> += 2
"""Other docs???
Is this allowed???
"""
"#,
);
// See the comment in the `hover_augmented_assignment` test above. The same
// reasoning applies here.
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Unknown | Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Unknown | Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:9:3
|
7 | """
8 |
9 | C.attr += 2
| ^^^^- Cursor offset
| |
| source
10 | """Other docs???
|
"#);
--> main.py:5:3
|
3 | attr = 1
4 |
5 | C.attr += 2
| ^^^^- Cursor offset
| |
| source
|
");
}
#[test]
@@ -2554,28 +2468,15 @@ def function():
r#"
class Foo:
a<CURSOR>: int
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:5
@@ -2585,9 +2486,8 @@ def function():
| ^- Cursor offset
| |
| source
4 | """This is the docs for this value
|
"#);
");
}
#[test]
@@ -2596,28 +2496,15 @@ def function():
r#"
class Foo:
a<CURSOR>: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:5
@@ -2627,52 +2514,7 @@ def function():
| ^- Cursor offset
| |
| source
4 | """This is the docs for this value
|
"#);
}
#[test]
fn hover_annotated_assignment_with_rhs_use() {
let test = cursor_test(
r#"
class Foo:
a: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
x = Foo()
x.a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:3
|
9 | x = Foo()
10 | x.a
| ^- Cursor offset
| |
| source
|
");
}
@@ -2683,28 +2525,15 @@ def function():
class Foo:
def __init__(self, a: int):
self.a<CURSOR>: int = a
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:14
@@ -2715,53 +2544,7 @@ def function():
| ^- Cursor offset
| |
| source
5 | """This is the docs for this value
|
"#);
}
#[test]
fn hover_annotated_attribute_assignment_use() {
let test = cursor_test(
r#"
class Foo:
def __init__(self, a: int):
self.a: int = a
"""This is the docs for this value
Wow these are good docs!
"""
x = Foo(1)
x.a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:11:3
|
10 | x = Foo(1)
11 | x.a
| ^- Cursor offset
| |
| source
|
");
}
@@ -2956,295 +2739,6 @@ def function():
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_func_with_concat_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
"""wow cool docs""" """and docs"""
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docsand docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docsand docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | """wow cool docs""" """and docs"""
4 | return
|
"#);
}
#[test]
fn hover_func_with_plus_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
"""wow cool docs""" + """and docs"""
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
```python
def ab() -> Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | """wow cool docs""" + """and docs"""
4 | return
|
"#);
}
#[test]
fn hover_func_with_slash_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
"""wow cool docs""" \
"""and docs"""
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docsand docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docsand docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | """wow cool docs""" \
4 | """and docs"""
|
"#);
}
#[test]
fn hover_func_with_sameline_commented_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
"""wow cool docs""" # and a comment
"""and docs""" # that shouldn't be included
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | """wow cool docs""" # and a comment
4 | """and docs""" # that shouldn't be included
|
"#);
}
#[test]
fn hover_func_with_nextline_commented_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
"""wow cool docs"""
# and a comment that shouldn't be included
"""and docs"""
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | """wow cool docs"""
4 | # and a comment that shouldn't be included
|
"#);
}
#[test]
fn hover_func_with_parens_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
(
"""wow cool docs"""
"""and docs"""
)
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docsand docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docsand docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | (
4 | """wow cool docs"""
|
"#);
}
#[test]
fn hover_func_with_nextline_commented_parens_docstring() {
let test = cursor_test(
r#"
def a<CURSOR>b():
(
"""wow cool docs"""
# and a comment that shouldn't be included
"""and docs"""
)
return
"#,
);
assert_snapshot!(test.hover(), @r#"
def ab() -> Unknown
---------------------------------------------
wow cool docsand docs
---------------------------------------------
```python
def ab() -> Unknown
```
---
wow cool docsand docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | def ab():
| ^-
| ||
| |Cursor offset
| source
3 | (
4 | """wow cool docs"""
|
"#);
}
#[test]
fn hover_attribute_docstring_spill() {
let test = cursor_test(
r#"
if True:
a<CURSOR>b = 1
"this shouldn't be a docstring but also it doesn't matter much"
"#,
);
assert_snapshot!(test.hover(), @r#"
Literal[1]
---------------------------------------------
```python
Literal[1]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:5
|
2 | if True:
3 | ab = 1
| ^-
| ||
| |Cursor offset
| source
4 | "this shouldn't be a docstring but also it doesn't matter much"
|
"#);
}
#[test]
fn hover_class_typevar_variance() {
let test = cursor_test(

View File

@@ -29,11 +29,10 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal
use ruff_python_codegen::Stylist;
use ruff_python_importer::Insertion;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_module_resolver::ModuleName;
use ty_project::Db;
use ty_python_semantic::semantic_index::definition::DefinitionKind;
use ty_python_types::types::Type;
use ty_python_types::{MemberDefinition, SemanticModel};
use ty_python_semantic::types::Type;
use ty_python_semantic::{MemberDefinition, ModuleName, SemanticModel};
pub(crate) struct Importer<'a> {
/// The ty Salsa database.
@@ -870,6 +869,7 @@ mod tests {
use insta::assert_snapshot;
use insta::internals::SettingsBindDropGuard;
use crate::find_node::covering_node;
use crate::tests::{CursorTest, CursorTestBuilder, cursor_test};
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{File, FileRootKind, system_path_to_file};
@@ -877,14 +877,14 @@ mod tests {
use ruff_db::source::source_text;
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_db::{Db, system};
use ruff_python_ast::find_node::covering_node;
use ruff_python_codegen::Stylist;
use ruff_python_trivia::textwrap::dedent;
use ruff_text_size::TextSize;
use ty_module_resolver::SearchPathSettings;
use ty_project::ProjectMetadata;
use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, PythonVersionWithSource};
use ty_python_types::SemanticModel;
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
SemanticModel,
};
use super::*;

View File

@@ -6,9 +6,9 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_types::types::ide_support::inlay_hint_call_argument_details;
use ty_python_types::types::{Type, TypeDetail};
use ty_python_types::{HasType, SemanticModel};
use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details;
use ty_python_semantic::types::{Type, TypeDetail};
use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)]
pub struct InlayHint {
@@ -1241,12 +1241,12 @@ mod tests {
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:8:5
@@ -1335,12 +1335,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:9:5
@@ -1391,12 +1391,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:10:5
@@ -2217,12 +2217,12 @@ mod tests {
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:2:5
@@ -2270,12 +2270,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:3:5
@@ -2325,12 +2325,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:5
@@ -2364,13 +2364,13 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2618:7
--> stdlib/builtins.pyi:2591:7
|
2617 | @final
2618 | class bool(int):
2590 | @final
2591 | class bool(int):
| ^^^^
2619 | """Returns True when the argument is true, False otherwise.
2620 | The builtins True and False are the only two instances of the class bool.
2592 | """Returns True when the argument is true, False otherwise.
2593 | The builtins True and False are the only two instances of the class bool.
|
info: Source
--> main2.py:4:20
@@ -2384,12 +2384,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:5:5
@@ -2443,12 +2443,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:6:5
@@ -2502,12 +2502,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:7:5
@@ -2561,12 +2561,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:8:5
@@ -2620,12 +2620,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:9:5
@@ -2678,12 +2678,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:10:5
@@ -2737,12 +2737,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:11:5
@@ -2811,12 +2811,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:12:5
@@ -2925,12 +2925,12 @@ mod tests {
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:7:5
@@ -3092,12 +3092,12 @@ mod tests {
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:18
@@ -3110,12 +3110,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:5:18
@@ -3248,12 +3248,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2722:7
--> stdlib/builtins.pyi:2695:7
|
2721 | @disjoint_base
2722 | class tuple(Sequence[_T_co]):
2694 | @disjoint_base
2695 | class tuple(Sequence[_T_co]):
| ^^^^^
2723 | """Built-in immutable sequence.
2696 | """Built-in immutable sequence.
|
info: Source
--> main2.py:8:5
@@ -4250,12 +4250,12 @@ mod tests {
foo([x=]y[0])
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:3:5
@@ -4303,12 +4303,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:5
@@ -5609,12 +5609,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:3:14
@@ -6046,13 +6046,13 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2618:7
--> stdlib/builtins.pyi:2591:7
|
2617 | @final
2618 | class bool(int):
2590 | @final
2591 | class bool(int):
| ^^^^
2619 | """Returns True when the argument is true, False otherwise.
2620 | The builtins True and False are the only two instances of the class bool.
2592 | """Returns True when the argument is true, False otherwise.
2593 | The builtins True and False are the only two instances of the class bool.
|
info: Source
--> main2.py:4:25
@@ -6100,12 +6100,12 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2829:7
--> stdlib/builtins.pyi:2802:7
|
2828 | @disjoint_base
2829 | class list(MutableSequence[_T]):
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2830 | """Built-in mutable sequence.
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:49
@@ -6614,6 +6614,13 @@ mod tests {
3 | y[: type[T@f]] = x
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def f[T](x: type[T]):
y: type[T@f] = x
"#);
}
@@ -6732,14 +6739,14 @@ mod tests {
A = TypeAliasType([name=]'A', [value=]str)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/typing.pyi:2037:26
--> stdlib/typing.pyi:2032:26
|
2035 | """
2036 |
2037 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
2030 | """
2031 |
2032 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
| ^^^^
2038 | @property
2039 | def __value__(self) -> Any: ... # AnnotationForm
2033 | @property
2034 | def __value__(self) -> Any: ... # AnnotationForm
|
info: Source
--> main2.py:3:20
@@ -6750,14 +6757,14 @@ mod tests {
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/typing.pyi:2037:37
--> stdlib/typing.pyi:2032:37
|
2035 | """
2036 |
2037 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
2030 | """
2031 |
2032 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
| ^^^^^
2038 | @property
2039 | def __value__(self) -> Any: ... # AnnotationForm
2033 | @property
2034 | def __value__(self) -> Any: ... # AnnotationForm
|
info: Source
--> main2.py:3:32

View File

@@ -8,6 +8,7 @@ mod completion;
mod doc_highlights;
mod docstring;
mod document_symbols;
mod find_node;
mod find_references;
mod goto;
mod goto_declaration;
@@ -57,7 +58,7 @@ use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::ops::{Deref, DerefMut};
use ty_project::Db;
use ty_python_types::types::{Type, TypeDefinition};
use ty_python_semantic::types::{Type, TypeDefinition};
/// Information associated with a text range.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]

View File

@@ -10,17 +10,17 @@
//! all references to these externally-visible symbols therefore requires
//! an expensive search of all source files in the workspace.
use crate::find_node::CoveringNode;
use crate::goto::GotoTarget;
use crate::{Db, NavigationTargets, ReferenceKind, ReferenceTarget};
use ruff_db::files::File;
use ruff_python_ast::find_node::CoveringNode;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{
self as ast, AnyNodeRef,
visitor::source_order::{SourceOrderVisitor, TraversalSignal},
};
use ruff_text_size::{Ranged, TextRange};
use ty_python_types::{ImportAliasResolution, SemanticModel};
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Mode for references search behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -334,7 +334,10 @@ impl LocalReferencesFinder<'_> {
/// Determines whether the given covering node is a reference to
/// the symbol we are searching for
fn check_reference_from_covering_node(&mut self, covering_node: &CoveringNode<'_>) {
fn check_reference_from_covering_node(
&mut self,
covering_node: &crate::find_node::CoveringNode<'_>,
) {
// Use the start of the covering node as the offset. Any offset within
// the node is fine here. Offsets matter only for import statements
// where the identifier might be a multi-part module name.

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