diff --git a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md index 03d60493ec..a511397bb4 100644 --- a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md +++ b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md @@ -25,6 +25,8 @@ scripts/collect_ty_ecosystem_run_metadata.py \ The manifest contains the analyzed Ruff revisions, Actions `EXCLUDE_NEWER`, ecosystem-analyzer and mypy-primer revisions, and each project's CI Python version. Stop if the helper cannot determine a unique value; never substitute a comment timestamp or local default. +The current workflow splits compilation into `Build ty (base)` and `Build ty (pr)`. The helper reads the base job, which records both the merge base and PR merge revision, and still supports historical runs with a single `Build ty` job. + ## Prepare ty If a primary agent supplied freshly copied base and PR binaries plus the PR ecosystem config, verify the paths exist and reuse them. Do not rebuild, switch Ruff refs, or overwrite the shared artifacts. diff --git a/.agents/skills/wobbling-ty-constraint-order/SKILL.md b/.agents/skills/wobbling-ty-constraint-order/SKILL.md new file mode 100644 index 0000000000..3b6b3588fe --- /dev/null +++ b/.agents/skills/wobbling-ty-constraint-order/SKILL.md @@ -0,0 +1,75 @@ +--- +name: wobbling-ty-constraint-order +description: > + Use when a user asks to wobble ty constraint ordering, check constraint-set or TDD ordering + determinism, test reversed constraint/typevar IDs, or investigate nondeterministic ty inference + and mdtest results. +compatibility: > + Requires Cargo, mktemp, and a POSIX-compatible shell; uses cargo-nextest when available and + otherwise falls back to cargo test. +--- + +# Wobbling ty constraint order + +`TY_CONSTRAINT_SET_ORDER` perturbs both the builder-local TDD-variable order and the local typevar order used to orient typevar-to-typevar constraints. The setting is fixed for the lifetime of each test process. + +- unset/`0`: normal ordering; +- `reverse`: reverse both orderings; +- an integer: XOR each local ID with that mask. Small masks immediately perturb dense arena IDs: `1` swaps adjacent IDs, `3` reverses blocks of four, and powers of two exchange neighboring blocks. + +This deliberately changes internal TDD shape. Run **mdtests only**: graph-structure unit snapshots are expected to differ. Never enable snapshot updates for a wobble run, since updating would hide the failures being sought. + +## Run + +From the Ruff root, first establish the normal baseline, then run the reversed and XOR-masked orders sequentially. Set `TY_CONSTRAINT_ORDER_LOG_DIR` to retain logs in a particular writable directory; otherwise `mktemp` chooses an appropriate temporary directory (respecting the environment's temporary-directory configuration). + +```bash +set -u + +if test -n "${TY_CONSTRAINT_ORDER_LOG_DIR:-}"; then + log_dir="$TY_CONSTRAINT_ORDER_LOG_DIR" + mkdir -p "$log_dir" +else + log_dir="$(mktemp -d -t ty-constraint-order.XXXXXXXX)" +fi +printf '%s\n' "logs: $log_dir" + +if cargo nextest --version >/dev/null 2>&1; then + runner=nextest +else + runner=test +fi +printf '%s\n' "runner: cargo $runner" + +export CARGO_PROFILE_DEV_OPT_LEVEL=1 +export CARGO_PROFILE_DEV_DEBUG=line-tables-only +export INSTA_UPDATE=no +export MDTEST_UPDATE_SNAPSHOTS=0 +unset INSTA_FORCE_PASS || true + +for order in normal reverse 1 2 3 4 7 8 15; do + if test "$order" = normal; then + unset TY_CONSTRAINT_SET_ORDER || true + else + export TY_CONSTRAINT_SET_ORDER="$order" + fi + + log="$log_dir/ty-constraint-order-${order}.log" + if test "$runner" = nextest; then + cargo nextest run -p ty_python_semantic --test mdtest \ + --no-fail-fast --status-level fail --failure-output immediate-final \ + >"$log" 2>&1 + else + cargo test -p ty_python_semantic --test mdtest >"$log" 2>&1 + fi + status=$? + + printf '%-7s exit=%s\n' "$order" "$status" + grep -E 'Summary \[|test result:' "$log" | tail -1 || true + printf '%s\n' " log: $log" +done +``` + +Read each failing log and report the mdtest file, section, line, expected result, and actual diagnostic/revealed type. A wobble failure is evidence that inference semantics or displayed solution types still depend on ordering; do not update the mdtest expectations merely to make the wobble run green. + +The knob does **not** perturb hashing of Salsa-backed values. The `Solution binding order follows constraint source order` section of `regression/constraint_set_ordering.md` separately varies typevar declaration order to catch binding-order changes caused by draining an `FxHashMap`. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index ed3abca41d..2528699099 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -8,7 +8,7 @@ self-hosted-runner: - depot-ubuntu-latest-8 - depot-ubuntu-22.04-16 - depot-ubuntu-22.04-32 - - depot-macos-15 + - namespace-profile-macos-15 - depot-windows-2022-16 - depot-ubuntu-22.04-arm-4 - github-windows-2025-x86_64-8 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index d399d74ec3..90aabdfa76 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -66,7 +66,7 @@ jobs: macos-x86_64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: macos-15 + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -96,7 +96,7 @@ jobs: macos-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: macos-15 + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 371292c95f..899cda4983 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -52,9 +52,9 @@ jobs: submodules: recursive persist-credentials: false - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} with: registry: ghcr.io @@ -78,7 +78,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ${{ env.RUFF_BASE_IMG }} # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name @@ -94,7 +94,7 @@ jobs: # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - name: Build and push by digest id: build - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: ${{ matrix.platform }} @@ -138,11 +138,11 @@ jobs: pattern: digests-* merge-multiple: true - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ${{ env.RUFF_BASE_IMG }} # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version @@ -150,7 +150,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -210,9 +210,9 @@ jobs: - debian:trixie-slim,trixie-slim,debian-slim - buildpack-deps:trixie,trixie,debian steps: - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -262,7 +262,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 # ghcr.io prefers index level annotations env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index @@ -275,7 +275,7 @@ jobs: - name: Build and push id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -316,11 +316,11 @@ jobs: pattern: digests-* merge-multiple: true - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: @@ -330,7 +330,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8d6e790d97..a93f721aef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -263,7 +263,7 @@ jobs: persist-credentials: false - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: - go-version: "1.26.4" + go-version: "1.26.5" - name: "Run ShellCheck" env: GOSUMDB: sum.golang.org @@ -336,15 +336,15 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: | cargo-nextest cargo-insta - name: "Install uv" - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -405,13 +405,13 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-nextest - name: "Install uv" - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -423,7 +423,7 @@ jobs: matrix: platform: - ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }} - - ${{ github.repository == 'astral-sh/ruff' && 'depot-macos-15' || 'macos-latest' }} + - ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-latest' }} name: "cargo test (${{ matrix.platform }})" runs-on: ${{ matrix.platform }} needs: determine_changes @@ -446,13 +446,13 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-nextest - name: "Install uv" - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" enable-cache: "true" - name: "Run tests" run: | @@ -474,7 +474,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 cache: "npm" @@ -561,9 +561,9 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: ruff-linux-debug @@ -606,9 +606,9 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -653,11 +653,11 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.11.28" + version: "0.11.31" - name: "Install Rust toolchain" run: rustup show @@ -805,9 +805,9 @@ jobs: # Faster to do this separately than to use `fetch-depth: 0` with `actions/checkout` - name: Fetch full history without tags run: git fetch --no-tags --filter=blob:none --unshallow origin - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -858,7 +858,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@732870f031d2fb36309d0deaf36abcc704a7be65 # v1.20.1 + - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 - run: cargo binstall --no-confirm cargo-shear@1.12.4 - run: cargo shear --deny-warnings @@ -874,9 +874,9 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -927,10 +927,10 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + version: "0.11.31" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 - name: "Cache prek" @@ -963,11 +963,11 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: Install uv - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: python-version: 3.13 activate-environment: true - version: "0.11.28" + version: "0.11.31" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1077,7 +1077,7 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 cache: "npm" @@ -1130,15 +1130,15 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-codspeed @@ -1146,7 +1146,7 @@ jobs: run: cargo codspeed build -m simulation -m memory --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 + uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 with: mode: "simulation,memory" run: cargo codspeed run @@ -1180,7 +1180,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-codspeed @@ -1233,12 +1233,12 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install codspeed" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-codspeed @@ -1253,7 +1253,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 + uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 with: mode: ${{ matrix.mode }} run: cargo codspeed run --bench "${{ matrix.target }}" "${{ matrix.filter }}" @@ -1286,15 +1286,15 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-codspeed @@ -1339,12 +1339,12 @@ jobs: with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install codspeed" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: cargo-codspeed @@ -1359,7 +1359,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 + uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index 73d21a914c..c7c67c1cfd 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -36,9 +36,9 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index c7df7f6734..32b0f8f1cb 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 package-manager-cache: false diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 07922571c8..4501473458 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -22,9 +22,9 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index bfcb95b5f6..e9ab1ff682 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -36,7 +36,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 package-manager-cache: false diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index 9abb85fa11..52e7ed28ec 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -31,7 +31,7 @@ jobs: run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions - name: "Install uv" - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: "Update versions" env: diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 02ff8fa310..3088ae5850 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -27,7 +27,7 @@ jobs: with: name: wasm-npm-${{ matrix.target }} path: pkg - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 5400dd3a8f..851d117002 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -84,9 +84,9 @@ jobs: run: | git config --global user.name typeshedbot git config --global user.email '<>' - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -140,9 +140,9 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: Setup git run: | git config --global user.name typeshedbot @@ -182,9 +182,9 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" - name: Setup git run: | git config --global user.name typeshedbot @@ -268,7 +268,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: | cargo-nextest diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 82b2c3d822..b5d8a56c22 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -42,7 +42,7 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: e2c5b76149b147fae104a7d8fa0997a9eb7f7754 + ECOSYSTEM_ANALYZER_COMMIT: 263b5500881186e8c918193577c23b341e5b7237 jobs: build-ty: @@ -135,9 +135,9 @@ jobs: TY_DISABLE_FLUID_SPECIALIZATIONS: "1" steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -196,9 +196,9 @@ jobs: timeout-minutes: 360 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.28" + version: "0.11.31" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 0844ef3ba0..e06ae6da2e 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -33,10 +33,10 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.11.28" + version: "0.11.31" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6773919191..e85a75a8e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: priority: 0 # Prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: 39e2973981e6d2f9b6c543b0086a2d2393abdc89 # frozen: v3.9.4 + rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5 hooks: - id: prettier types: [yaml] @@ -63,7 +63,7 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: e3eebf65325ccc992422292cb7a4baee967cf815 # frozen: v1.26.1 + rev: 64a97fb7fa63188393d3215c6e312f5f9c6d0f78 # frozen: v1.27.0 hooks: - id: zizmor priority: 0 @@ -102,7 +102,7 @@ repos: - id: mdformat language: python # means renovate will also update `additional_dependencies` additional_dependencies: - - mdformat-mkdocs==5.2.0 + - mdformat-mkdocs==5.2.1 - mdformat-footnote==0.1.3 exclude: | (?x)^( @@ -113,13 +113,13 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: d9fca3320346514799461a80b0753eb45d707d46 # frozen: 0.11.28 + rev: 69e5d7b46d7a93b633431a498a46cf3a8a2181f4 # frozen: 0.11.31 hooks: - id: uv-lock priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 hooks: - id: ruff-format exclude: crates/ty_python_semantic/resources/corpus/ @@ -127,7 +127,7 @@ repos: # Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] @@ -137,7 +137,7 @@ repos: priority: 1 - repo: https://github.com/igorshubovych/markdownlint-cli - rev: a4d5d37e66ebcd6b3705204a1d6dbb56dea66338 # frozen: v0.49.0 + rev: 5b5dddc4fb0f83c3ea1fc5616fa63e115dce83e0 # frozen: v0.49.1 hooks: - id: markdownlint-fix exclude: | @@ -150,7 +150,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 hooks: - id: ruff-format name: mdtest format diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e8150171..e92d24f1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,117 @@ # Changelog +## 0.15.22 + +Released on 2026-07-16. + +### Preview features + +- \[`pycodestyle`\] Add an autofix for `E402` ([#22212](https://github.com/astral-sh/ruff/pull/22212)) +- \[`refurb`\] Allow subclassing builtins in stub files (`FURB189`) ([#26812](https://github.com/astral-sh/ruff/pull/26812)) +- \[`ruff`\] Add rule to replace `noqa` comments with `ruff:ignore` (`RUF105`) ([#26423](https://github.com/astral-sh/ruff/pull/26423)) +- \[`ruff`\] Add rule to use human-readable names in `ruff:ignore` comments (`RUF106`) ([#26682](https://github.com/astral-sh/ruff/pull/26682)) +- \[`ruff`\] Add rule to use human-readable names in configuration selectors (`RUF201`) ([#26772](https://github.com/astral-sh/ruff/pull/26772)) + +### Bug fixes + +- \[`flake8-pyi`\] Fix false positive in `__all__` (`PYI053`) ([#26872](https://github.com/astral-sh/ruff/pull/26872)) + +### Rule changes + +- \[`pylint`\] Ignore mutable type updates in `redefined-loop-name` (`PLW2901`) ([#25733](https://github.com/astral-sh/ruff/pull/25733)) + +### Performance + +- Avoid redundant lexer token bookkeeping ([#26765](https://github.com/astral-sh/ruff/pull/26765)) +- Avoid redundant pending-indentation writes ([#26774](https://github.com/astral-sh/ruff/pull/26774)) +- Avoid unnecessary identifier lookahead ([#26525](https://github.com/astral-sh/ruff/pull/26525)) +- Reuse parser scratch buffers ([#26798](https://github.com/astral-sh/ruff/pull/26798)) + +### Documentation + +- Document argfile support ([#26803](https://github.com/astral-sh/ruff/pull/26803)) +- \[`flake8-datetimez`\] Clarify naming guidance for `datetime.today` (`DTZ002`) ([#26658](https://github.com/astral-sh/ruff/pull/26658)) +- \[`pycodestyle`\] Document `E731` fix safety ([#26847](https://github.com/astral-sh/ruff/pull/26847)) +- \[`ruff`\] Clarify intentional async contexts for `unused-async` (`RUF029`) ([#26641](https://github.com/astral-sh/ruff/pull/26641)) + +### Contributors + +- [@dwego](https://github.com/dwego) +- [@MichaReiser](https://github.com/MichaReiser) +- [@Joosboy](https://github.com/Joosboy) +- [@KaufmanDmitriy](https://github.com/KaufmanDmitriy) +- [@PeterJCLaw](https://github.com/PeterJCLaw) +- [@ntBre](https://github.com/ntBre) +- [@charliermarsh](https://github.com/charliermarsh) + +## 0.15.21 + +Released on 2026-07-09. + +### Preview features + +- Add `--add-ignore` for adding `ruff:ignore` comments ([#26346](https://github.com/astral-sh/ruff/pull/26346)) +- \[`flake8-comprehensions`\] Drop `C409` tuple comprehension preview behavior ([#25707](https://github.com/astral-sh/ruff/pull/25707)) +- Avoid whitespace normalization when formatting comments ([#26455](https://github.com/astral-sh/ruff/pull/26455)) +- \[`pyupgrade`\] Lint and fix use of deprecated `abc` decorators (`UP051`) ([#26417](https://github.com/astral-sh/ruff/pull/26417)) + +### Bug fixes + +- Refine non-empty f-string detection ([#26526](https://github.com/astral-sh/ruff/pull/26526)) +- Detect syntax errors in individual notebook cells ([#26419](https://github.com/astral-sh/ruff/pull/26419)) +- \[`flake8-implicit-str-concat`\] Fix `ISC003` autofix incorrectly stripping `+` from comments ([#26554](https://github.com/astral-sh/ruff/pull/26554)) + +### Rule changes + +- \[`flake8-executable`\] Mark `EXE004` fix as unsafe ([#26033](https://github.com/astral-sh/ruff/pull/26033)) +- \[`flake8-pyi`\] Mark `PYI061` fixes as unsafe in Python files ([#26533](https://github.com/astral-sh/ruff/pull/26533)) +- \[`pydocstyle`\] Skip `overload-with-docstring` in stub files (`D418`) ([#26318](https://github.com/astral-sh/ruff/pull/26318)) + +### Performance + +- Avoid per-token source index visitor calls ([#26506](https://github.com/astral-sh/ruff/pull/26506)) +- Cache parenthesized expression boundaries in the formatter ([#26344](https://github.com/astral-sh/ruff/pull/26344)) +- Improve performance of rendering edits in preview mode ([#26565](https://github.com/astral-sh/ruff/pull/26565)) +- Inline `fits_element` in formatter ([#26429](https://github.com/astral-sh/ruff/pull/26429)) +- Inline formatter printing hot paths ([#26504](https://github.com/astral-sh/ruff/pull/26504)) +- Lazily create builtin bindings ([#26510](https://github.com/astral-sh/ruff/pull/26510)) +- Skip empty trivia scans in the source indexer ([#26507](https://github.com/astral-sh/ruff/pull/26507)) +- Use ICF for macOS release builds ([#25780](https://github.com/astral-sh/ruff/pull/25780)) + +### Formatter + +- Add `--extend-exclude` to `ruff format` ([#26372](https://github.com/astral-sh/ruff/pull/26372)) + +### Documentation + +- Add "How does Ruff's import sorting compare to isort?" link to README ([#26530](https://github.com/astral-sh/ruff/pull/26530)) +- Fix Mozilla Firefox repository link in README ([#26537](https://github.com/astral-sh/ruff/pull/26537)) +- \[`flake8-bandit`\] Fix misleading docstring for `mako-templates` (`S702`) ([#26432](https://github.com/astral-sh/ruff/pull/26432)) +- \[`ruff`\] Fix non-triggering example for `if-key-in-dict-del` (`RUF051`) ([#26433](https://github.com/astral-sh/ruff/pull/26433)) + +### Contributors + +- [@EkriirkE](https://github.com/EkriirkE) +- [@tingerrr](https://github.com/tingerrr) +- [@s-rigaud](https://github.com/s-rigaud) +- [@nikolauspschuetz](https://github.com/nikolauspschuetz) +- [@Avasam](https://github.com/Avasam) +- [@ntBre](https://github.com/ntBre) +- [@omar-y-abdi](https://github.com/omar-y-abdi) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@sylvestre](https://github.com/sylvestre) +- [@shaanmajid](https://github.com/shaanmajid) +- [@lerebear](https://github.com/lerebear) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@Sanjays2402](https://github.com/Sanjays2402) +- [@ZedThree](https://github.com/ZedThree) +- [@servusdei2018](https://github.com/servusdei2018) +- [@charliermarsh](https://github.com/charliermarsh) +- [@jesco-absolute](https://github.com/jesco-absolut) +- [@velikodniy](https://github.com/velikodniy) +- [@zaniebot](https://github.com/zaniebot) +- [@epage](https://github.com/epage) + ## 0.15.20 Released on 2026-06-25. diff --git a/Cargo.lock b/Cargo.lock index af100f1593..1b45e9e6f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,21 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstream" version = "1.0.0" @@ -64,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -80,13 +95,22 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-lossy" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ca7d0f520afcd6d817970d0b2d5fd7c630c75e7783cae046b8b8a783c5befa" +checksum = "04d3a5dc826f84d0ea11882bb8054ff7f3d482602e11bb181101303a279ea01f" dependencies = [ "anstyle", ] +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-parse" version = "1.0.0" @@ -98,35 +122,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-svg" -version = "1.1.0" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7ec5fa88fc0837393f6b110b0764aeb0a673952bc91300628641dd0fffbf85" +checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde" dependencies = [ "anstyle", "anstyle-lossy", - "anstyle-parse", + "anstyle-parse 0.2.7", "html-escape", "unicode-width", ] [[package]] name = "anstyle-wincon" -version = "3.0.11" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -196,9 +220,9 @@ dependencies = [ [[package]] name = "attribute-derive" -version = "0.10.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54" dependencies = [ "attribute-derive-macro", "derive-where", @@ -210,9 +234,9 @@ dependencies = [ [[package]] name = "attribute-derive-macro" -version = "0.10.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b" dependencies = [ "collection_literals", "interpolator", @@ -226,9 +250,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bit-set" @@ -272,6 +296,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -289,9 +322,9 @@ checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", @@ -300,9 +333,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.3" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "by_override_patch" @@ -373,9 +406,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.12.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cachedir" @@ -412,9 +445,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -436,24 +469,37 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures", - "rand_core 0.10.1", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "char_str" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576ba56f6ca18ebb069d0d07260407171712aff4dfe15ee3f8982dc16a455bcf" +dependencies = [ + "castaway", + "get-size2", + "itoa", + "ryu", + "serde_core", ] [[package]] name = "chrono" -version = "0.4.45" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -499,7 +545,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -508,9 +554,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.7" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" +checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" dependencies = [ "clap", ] @@ -528,9 +574,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.6.0" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897" +checksum = "0a0c951694691e65bf9d421d597d68416c22de9632e884c28412cb8cd8b73dce" dependencies = [ "clap", "clap_complete", @@ -550,9 +596,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clearscreen" @@ -564,7 +610,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -576,7 +622,7 @@ dependencies = [ "anyhow", "cc", "colored", - "getrandom 0.4.3", + "getrandom 0.4.2", "glob", "libc", "nix", @@ -667,15 +713,15 @@ dependencies = [ [[package]] name = "collection_literals" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" +checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8" [[package]] name = "colorchoice" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -683,22 +729,21 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] name = "compact_str" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" dependencies = [ "castaway", "cfg-if", "itoa", - "rustversion", - "ryu", "serde", "static_assertions", + "zmij", ] [[package]] @@ -716,7 +761,7 @@ dependencies = [ "encode_unicode", "libc", "unicode-width", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -751,6 +796,15 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -827,18 +881,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -846,27 +900,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -874,6 +928,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.4.0" @@ -888,9 +952,9 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.13" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" dependencies = [ "memchr", ] @@ -903,7 +967,7 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", "nix", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -928,15 +992,15 @@ checksum = "a867d7322eb69cf3a68a5426387a25b45cb3b9c5ee41023ee6cea92e2afadd82" dependencies = [ "camino", "fancy-regex", - "libtest-mimic", + "libtest-mimic 0.8.1", "walkdir", ] [[package]] name = "defmt" -version = "1.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -944,11 +1008,12 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" dependencies = [ "defmt-parser", + "proc-macro-error2", "proc-macro2", "quote", "syn", @@ -965,9 +1030,9 @@ dependencies = [ [[package]] name = "derive-where" -version = "1.6.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", @@ -986,6 +1051,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1004,14 +1079,14 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] name = "dispatch2" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.13.0", "block2", @@ -1021,9 +1096,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", @@ -1061,9 +1136,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -1084,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -1111,7 +1186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -1127,9 +1202,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fern" @@ -1158,12 +1233,13 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ + "crc32fast", + "libz-rs-sys", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -1195,9 +1271,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", ] @@ -1243,20 +1319,30 @@ dependencies = [ [[package]] name = "gen-lsp-types" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946cd448051e8061e4e17d88ecaceea8d84c33da52b93a9391b4a657def1b8bd" +checksum = "4cd635c5206acd03ea024d6b5902539e5c903de3afa220fdb5c94b583af77f4f" dependencies = [ "serde", "serde_json", "url", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "get-size-derive2" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1da24fbda09ec01bca7cfa1797c0e520e75123bccb01dcdf9041f8aa65183bc2" +checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", @@ -1265,15 +1351,16 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823645bc6404ae2915707777061a47d3a031a9ee0bff51b34ec973df3d8d2990" +checksum = "b411f34418305908ab15a82ff78958c2a9aee9a272b2a2e663836b20a4e4b9d3" dependencies = [ "compact_str", "get-size-derive2", "hashbrown 0.17.1", "indexmap", "ordermap", + "parking_lot", "smallvec", "thin-vec", ] @@ -1289,9 +1376,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1300,14 +1387,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core 0.10.1", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", ] [[package]] @@ -1342,13 +1443,12 @@ dependencies = [ [[package]] name = "half" -version = "2.7.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", - "zerocopy", ] [[package]] @@ -1379,9 +1479,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" dependencies = [ "hashbrown 0.17.1", ] @@ -1400,15 +1500,18 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "html-escape" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c1ff2d1cbf39efe5af0900ced8a069b5e61557a17544eb0c4a50239937389e" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] [[package]] name = "iana-time-zone" -version = "0.1.65" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1510,6 +1613,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1523,9 +1632,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1533,9 +1642,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7" +checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" dependencies = [ "crossbeam-deque", "globset", @@ -1604,9 +1713,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ "bitflags 2.13.0", "inotify-sys", @@ -1615,9 +1724,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] @@ -1659,9 +1768,9 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "intrusive-collections" -version = "0.10.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b719c59241cfaac1042a6d26787e28ed7ee4a4e21a5a907786f54222d1b0062" +checksum = "80e165935eba36cb526af8389effd2005a741adcbb6ed32106cc68e3f7b92960" dependencies = [ "memoffset", ] @@ -1689,20 +1798,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.17" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -1742,9 +1851,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" @@ -1759,7 +1868,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1775,9 +1884,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" @@ -1790,11 +1899,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.3.4", "libc", ] @@ -1806,9 +1915,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ "cfg-if", "futures-util", @@ -1817,9 +1926,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -1827,11 +1936,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.1.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ - "bitflags 2.13.0", + "bitflags 1.3.2", "libc", ] @@ -1841,6 +1950,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -1889,25 +2004,47 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ + "bitflags 2.13.0", "libc", ] [[package]] name = "libtest-mimic" -version = "0.8.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" dependencies = [ - "anstream", + "clap", + "escape8259", + "termcolor", + "threadpool", +] + +[[package]] +name = "libtest-mimic" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" +dependencies = [ + "anstream 0.6.21", "anstyle", "clap", "escape8259", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1916,9 +2053,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" @@ -1937,9 +2074,9 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lsp-server" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad8be6fe0ca81b8298bfbbe8a77e9fcd8895ad6c84cd7794d5ebadcbb09ae43" +checksum = "3ee25a31f2e571e426eef2896179450cafc7e2f5be00d8a93b1c2d21c0ff7656" dependencies = [ "crossbeam-channel", "log", @@ -2021,7 +2158,8 @@ dependencies = [ "similar 3.1.1", "smallvec", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", + "toml_parser", "tracing", ] @@ -2072,19 +2210,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] name = "mio" -version = "1.2.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2124,9 +2261,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.3" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags 2.13.0", "cfg-if", @@ -2170,20 +2307,17 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.13.0", -] +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" [[package]] name = "nu-ansi-term" -version = "0.50.3" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2196,11 +2330,21 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -2219,9 +2363,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "oorandom" @@ -2248,19 +2392,19 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.3" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "os_str_bytes" -version = "7.2.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89284d0c2af7b0eb5e814798aa07265413c8fd72009f7fc82ea25a81fb287ce9" +checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c" dependencies = [ "memchr", ] @@ -2295,7 +2439,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2306,21 +2450,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +checksum = "f808742975794703469f67a28dd14b1d1009a1743c18b0353b4b951dbb0068ad" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] +checksum = "03351d0f1c066c114015408dc6a3e101f080fc03ef9d8d799aa58ec760cac5a6" [[package]] name = "path-slash" @@ -2336,9 +2477,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "peg" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" dependencies = [ "peg-macros", "peg-runtime", @@ -2346,9 +2487,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" dependencies = [ "peg-runtime", "proc-macro2", @@ -2357,9 +2498,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" [[package]] name = "pep440_rs" @@ -2404,19 +2545,20 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", + "thiserror 2.0.18", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -2424,9 +2566,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", @@ -2437,11 +2579,12 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", + "sha2", ] [[package]] @@ -2480,7 +2623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.7", + "rand 0.8.5", ] [[package]] @@ -2503,15 +2646,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" @@ -2521,18 +2664,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2583,6 +2726,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -2592,6 +2745,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -2721,6 +2896,12 @@ dependencies = [ "syn", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -2735,18 +2916,18 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rancor" -version = "0.1.2" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ "ptr_meta", ] [[package]] name = "rand" -version = "0.8.7" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", @@ -2760,8 +2941,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -2780,14 +2961,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rayon" @@ -2811,9 +2992,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags 2.13.0", ] @@ -2824,7 +3005,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "libredox", "thiserror 2.0.18", ] @@ -2874,9 +3055,9 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" @@ -2886,9 +3067,9 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rend" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" dependencies = [ "bytecheck", ] @@ -2925,9 +3106,9 @@ dependencies = [ [[package]] name = "ron" -version = "0.12.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ "bitflags 2.13.0", "once_cell", @@ -2939,7 +3120,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.15.22" dependencies = [ "anyhow", "argfile", @@ -2995,7 +3176,7 @@ dependencies = [ "test-case", "thiserror 2.0.18", "tikv-jemallocator", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tracing", "walkdir", "wild", @@ -3003,15 +3184,15 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.3" +version = "0.0.5" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "memchr", "ruff_annotate_snippets", "serde", "snapbox", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tryfn", "unicode-width", ] @@ -3044,8 +3225,9 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.3" +version = "0.0.5" dependencies = [ + "char_str", "filetime", "glob", "globset", @@ -3057,7 +3239,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anstyle", "arc-swap", @@ -3135,7 +3317,7 @@ dependencies = [ "similar 3.1.1", "strum", "tempfile", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3148,7 +3330,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "is-macro", @@ -3158,7 +3340,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.3" +version = "0.0.5" dependencies = [ "drop_bomb", "ruff_cache", @@ -3174,7 +3356,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -3195,7 +3377,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "ruff_macros", @@ -3205,7 +3387,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.20" +version = "0.15.22" dependencies = [ "aho-corasick", "anyhow", @@ -3258,7 +3440,7 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "typed-arena", "unicode-normalization", "unicode-width", @@ -3268,7 +3450,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.3" +version = "0.0.5" dependencies = [ "heck", "itertools 0.15.0", @@ -3281,7 +3463,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.3" +version = "0.0.5" dependencies = [ "insta", "regex", @@ -3312,17 +3494,16 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", - "itertools 0.15.0", "rand 0.10.2", "ruff_diagnostics", "ruff_source_file", @@ -3336,18 +3517,19 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.3" +version = "0.0.5" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.3" +version = "0.0.5" dependencies = [ "aho-corasick", "arrayvec", "bitflags 2.13.0", + "char_str", "compact_str", "get-size2", "is-macro", @@ -3379,7 +3561,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3391,7 +3573,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -3424,7 +3606,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "insta", @@ -3439,7 +3621,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3450,7 +3632,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -3460,13 +3642,13 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "bitflags 2.13.0", "bstr", - "compact_str", "datatest-stable", + "drop_bomb", "get-size2", "insta", "itertools 0.15.0", @@ -3489,7 +3671,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags 2.13.0", "insta", @@ -3510,7 +3692,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -3518,7 +3700,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.3" +version = "0.0.5" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -3539,19 +3721,19 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "ruff_db", "ruff_text_size", "schemars", "serde", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", ] [[package]] name = "ruff_server" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "crossbeam", @@ -3586,7 +3768,7 @@ dependencies = [ "smallvec", "tempfile", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tracing", "tracing-log", "tracing-subscriber", @@ -3594,7 +3776,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "memchr", @@ -3604,7 +3786,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "schemars", @@ -3615,7 +3797,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.20" +version = "0.15.22" dependencies = [ "console_error_panic_hook", "console_log", @@ -3642,7 +3824,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "colored", @@ -3679,7 +3861,7 @@ dependencies = [ "shellexpand", "strum", "tempfile", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "unicode-normalization", ] @@ -3715,26 +3897,26 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbaab832e2ea754afda4a738f987dd1e8bd30c9e5d8c981ee6a3934386095e2" +checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" dependencies = [ "boxcar", "compact_str", @@ -3759,20 +3941,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6872462ac73d39969a836273c24163e6a26a4e08f5114fcd80e25af30ea9c6" +checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" [[package]] name = "salsa-macros" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc78ffaf65b1a9175818592c5130aa10b1bb245a905722fd4db87cea8a8457" +checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" dependencies = [ "proc-macro2", "quote", "syn", - "synstructure", ] [[package]] @@ -3821,6 +4002,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -3904,6 +4091,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3928,12 +4126,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "simdutf8" version = "0.1.5" @@ -3957,9 +4149,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -3975,11 +4167,11 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "snapbox" -version = "1.2.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de56eb4784a2c5c1efede55a0f06fbf481bc12b42b355b9525c15db1e3581a8" +checksum = "71d70a71b68054cbe88708f77abfc4bd2daf75028f8f55f4f1cff63565df89ea" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "anstyle-svg", "escargot", @@ -3987,26 +4179,26 @@ dependencies = [ "normalize-line-endings", "os_pipe", "serde_json", - "similar 3.1.1", + "similar 2.7.0", "snapbox-macros", "wait-timeout", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "snapbox-macros" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4a172e483585ebbc7c7f7d1705ca7e3f94f606ed78caa14805673189fd5455" +checksum = "d248cef42e1456ab2f7149c0376985351b7d849ea9ad2a957bf15ddfebf1fdf9" dependencies = [ - "anstream", + "anstream 0.6.21", ] [[package]] name = "stable_deref_trait" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" @@ -4068,9 +4260,9 @@ checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -4101,20 +4293,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.61.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", ] [[package]] name = "terminal_size" -version = "0.4.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4219,13 +4420,22 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tikv-jemalloc-sys" version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" @@ -4268,9 +4478,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -4293,14 +4503,14 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow 0.7.13", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", @@ -4308,7 +4518,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.0", ] [[package]] @@ -4338,7 +4548,7 @@ dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.0", ] [[package]] @@ -4347,14 +4557,14 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -4444,12 +4654,12 @@ dependencies = [ [[package]] name = "tryfn" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10406800cf2fa9941073c17412cda1af293ad82f19fea7060f4ea7fee267a14" +checksum = "f68b00518dd6c69ee2289900b140e55dad068cb925678603bfa8d539f61ef6c1" dependencies = [ "ignore", - "libtest-mimic", + "libtest-mimic 0.7.3", "snapbox", ] @@ -4484,7 +4694,7 @@ dependencies = [ "serde_json", "tempfile", "tikv-jemallocator", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tracing", "tracing-flame", "tracing-subscriber", @@ -4501,7 +4711,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ordermap", "ruff_db", @@ -4535,7 +4745,7 @@ dependencies = [ "ruff_text_size", "serde", "tempfile", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "ty_ide", "ty_module_resolver", "ty_project", @@ -4584,7 +4794,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "camino", @@ -4619,10 +4829,11 @@ dependencies = [ "crossbeam", "get-size2", "globset", - "ignore", "insta", + "memchr", "notify", "ordermap", + "parking_lot", "pep440_rs", "rayon", "regex", @@ -4645,7 +4856,7 @@ dependencies = [ "strum", "strum_macros", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "tracing", "ty_combine", "ty_module_resolver", @@ -4657,11 +4868,12 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "bitflags 2.13.0", "bitvec", + "char_str", "get-size2", "hashbrown 0.17.1", "itertools 0.15.0", @@ -4690,11 +4902,12 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "bitflags 2.13.0", "camino", + "char_str", "compact_str", "datatest-stable", "drop_bomb", @@ -4736,6 +4949,7 @@ dependencies = [ "ty_module_resolver", "ty_python_core", "ty_site_packages", + "ty_static", "ty_test", "ty_vendored", "unicode-segmentation", @@ -4783,7 +4997,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.3" +version = "0.0.5" dependencies = [ "camino", "colored", @@ -4804,7 +5018,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_macros", ] @@ -4826,6 +5040,7 @@ dependencies = [ "salsa", "serde", "tempfile", + "toml 1.1.3+spec-1.1.0", "tracing", "ty_module_resolver", "ty_python_core", @@ -4835,7 +5050,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.3" +version = "0.0.5" dependencies = [ "path-slash", "ruff_db", @@ -4886,6 +5101,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -4906,9 +5127,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -4925,6 +5146,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_names2" version = "1.3.0" @@ -4944,14 +5171,14 @@ dependencies = [ "getopts", "log", "phf_codegen", - "rand 0.8.7", + "rand 0.8.5", ] [[package]] name = "unit-prefix" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" [[package]] name = "unscanny" @@ -4978,6 +5205,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5008,13 +5241,19 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-ranges" -version = "0.1.3" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d" dependencies = [ "smallvec", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "vt100" version = "0.16.2" @@ -5070,11 +5309,29 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -5085,9 +5342,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" dependencies = [ "js-sys", "wasm-bindgen", @@ -5095,9 +5352,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5105,9 +5362,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -5118,18 +5375,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.76" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" +checksum = "e28a0782b173cf2e98f62aacb487c5c021b7c5925df46098675f28e1fa85e159" dependencies = [ "async-trait", "cast", @@ -5149,9 +5406,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.76" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" +checksum = "ee997551ce1ad5adda03f7ce37ec34b4140fe9f547fd07b46d55901d1ba1a06b" dependencies = [ "proc-macro2", "quote", @@ -5160,15 +5417,49 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.126" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" +checksum = "ae0f005eb61f765eff31a79ef71df7e21819731268a868df41192c1e41d6d3e5" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" dependencies = [ "js-sys", "wasm-bindgen", @@ -5224,7 +5515,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -5235,22 +5526,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.62.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.2.1", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -5259,15 +5550,21 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" @@ -5276,20 +5573,38 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.5.1" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-link", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -5298,103 +5613,261 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.3", ] [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "windows-targets" -version = "0.53.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link 0.1.3", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.1" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.53.1" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.1" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.53.1" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.15" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -5413,9 +5886,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5436,18 +5909,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -5456,18 +5929,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -5524,15 +5997,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 1705635073..78cd9b5774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ resolver = "2" [workspace.package] # Please update rustfmt.toml when bumping the Rust edition edition = "2024" -rust-version = "1.94" +rust-version = "1.95" homepage = "https://kotlinisland.github.io/basedpython" documentation = "https://kotlinisland.github.io/basedpython" repository = "https://github.com/KotlinIsland/basedpython" @@ -18,51 +18,52 @@ license = "MIT" [workspace.dependencies] by_transforms = { path = "crates/by_transforms" } -ruff = { version = "0.15.20", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.3", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.3", path = "crates/ruff_cache" } -ruff_db = { version = "0.0.3", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.3", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.3", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.3", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.3", path = "crates/ruff_index" } -ruff_linter = { version = "0.15.20", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.3", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.3", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.3", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.3", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.3", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.3", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.3", path = "crates/ruff_python_codegen" } -ruff_python_formatter = { version = "0.0.3", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.3", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.3", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.3", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.3", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.3", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.3", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.3", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.3", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.3", path = "crates/ruff_source_file" } +char_str = { version = "0.0.2" } +ruff = { version = "0.15.22", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.5", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.5", path = "crates/ruff_cache" } +ruff_db = { version = "0.0.5", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.5", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.5", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.5", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.5", path = "crates/ruff_index" } +ruff_linter = { version = "0.15.22", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.5", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.5", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.5", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.5", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.5", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.5", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.5", path = "crates/ruff_python_codegen" } +ruff_python_formatter = { version = "0.0.5", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.5", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.5", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.5", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.5", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.5", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.5", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.5", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.5", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.5", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.3", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.3", path = "crates/ruff_text_size" } -ruff_workspace = { version = "0.0.3", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.5", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.5", path = "crates/ruff_text_size" } +ruff_workspace = { version = "0.0.5", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.3", path = "crates/ty_combine" } +ty_combine = { version = "0.0.5", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.3", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.5", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.3", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.3", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.5", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.5", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.3", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.3", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.5", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.5", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.3", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.5", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } @@ -87,7 +88,7 @@ clap_complete_command = { version = "0.6.0" } clearscreen = { version = "4.0.0" } codspeed-criterion-compat = { version = "5.0.0", default-features = false } colored = { version = "3.0.0" } -compact_str = "0.9.0" +compact_str = "0.10.0" console_error_panic_hook = { version = "0.1.7" } console_log = { version = "1.0.0" } countme = { version = "3.0.1" } @@ -102,7 +103,7 @@ dunce = { version = "1.0.5" } etcetera = { version = "0.11.0" } fern = { version = "0.7.0" } filetime = { version = "0.2.23" } -get-size2 = { version = "0.10.0", features = [ +get-size2 = { version = "0.10.3", features = [ "derive", "smallvec", "hashbrown", @@ -118,7 +119,7 @@ hashbrown = { version = "0.17.0", default-features = false, features = [ ] } heck = "0.5.0" icu_properties = { version = "2.1.2" } -ignore = { version = "0.4.24" } +ignore = { version = "0.4.30" } imara-diff = { version = "0.2.0" } imperative = { version = "1.0.4" } indexmap = { version = "2.6.0" } @@ -134,15 +135,16 @@ js-sys = { version = "0.3.69" } libc = { version = "0.2.153" } libcst = { version = "1.8.4", default-features = false } log = { version = "0.4.17" } -lsp-server = { version = "0.8.0" } -lsp-types = { package = "gen-lsp-types", version = "0.9.0", features = ["url"] } +lsp-server = { version = "0.10.0" } +lsp-types = { package = "gen-lsp-types", version = "0.10.0", features = ["url"] } matchit = { version = "0.9.0" } memchr = { version = "2.7.1" } mimalloc = { version = "0.1.49", features = ["v2"] } natord = { version = "1.0.9" } notify = { version = "8.0.0" } ordermap = { version = "1.0.0" } -path-absolutize = { version = "3.1.1" } +parking_lot = { version = "0.12.4" } +path-absolutize = { version = "4.0.0" } path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } pep440_rs = { version = "0.7.1" } @@ -161,7 +163,7 @@ regex-syntax = { version = "0.8.8" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the version in `fuzz/Cargo.toml` -salsa = { version = "0.27.2", default-features = false, features = [ +salsa = { version = "0.28.1", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", @@ -194,6 +196,7 @@ thiserror = { version = "2.0.0" } thin-vec = { version = "0.2.14" } tikv-jemallocator = { version = "0.6.0" } toml = { version = "1.0.0" } +toml_parser = { version = "1.0.0" } tracing = { version = "0.1.40" } tracing-flame = { version = "0.2.0" } tracing-indicatif = { version = "0.3.11" } diff --git a/crates/basedpython/Cargo.lock b/crates/basedpython/Cargo.lock index ee33b7bdd7..4c3d602be0 100644 --- a/crates/basedpython/Cargo.lock +++ b/crates/basedpython/Cargo.lock @@ -72,7 +72,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -83,7 +83,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -328,6 +328,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "char_str" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576ba56f6ca18ebb069d0d07260407171712aff4dfe15ee3f8982dc16a455bcf" +dependencies = [ + "castaway", + "get-size2", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "chrono" version = "0.4.44" @@ -420,7 +433,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -441,22 +454,21 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "compact_str" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" dependencies = [ "castaway", "cfg-if", "itoa", - "rustversion", - "ryu", "serde", "static_assertions", + "zmij", ] [[package]] @@ -611,7 +623,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -680,7 +692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -801,9 +813,9 @@ dependencies = [ [[package]] name = "gen-lsp-types" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946cd448051e8061e4e17d88ecaceea8d84c33da52b93a9391b4a657def1b8bd" +checksum = "4cd635c5206acd03ea024d6b5902539e5c903de3afa220fdb5c94b583af77f4f" dependencies = [ "serde", "serde_json", @@ -812,9 +824,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1da24fbda09ec01bca7cfa1797c0e520e75123bccb01dcdf9041f8aa65183bc2" +checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", @@ -823,15 +835,16 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823645bc6404ae2915707777061a47d3a031a9ee0bff51b34ec973df3d8d2990" +checksum = "b411f34418305908ab15a82ff78958c2a9aee9a272b2a2e663836b20a4e4b9d3" dependencies = [ "compact_str", "get-size-derive2", "hashbrown 0.17.1", "indexmap", "ordermap", + "parking_lot", "smallvec", "thin-vec", ] @@ -1087,9 +1100,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" dependencies = [ "crossbeam-deque", "globset", @@ -1401,9 +1414,9 @@ checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lsp-server" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad8be6fe0ca81b8298bfbbe8a77e9fcd8895ad6c84cd7794d5ebadcbb09ae43" +checksum = "3ee25a31f2e571e426eef2896179450cafc7e2f5be00d8a93b1c2d21c0ff7656" dependencies = [ "crossbeam-channel", "log", @@ -1582,7 +1595,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1678,21 +1691,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +checksum = "f808742975794703469f67a28dd14b1d1009a1743c18b0353b4b951dbb0068ad" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] +checksum = "03351d0f1c066c114015408dc6a3e101f080fc03ef9d8d799aa58ec760cac5a6" [[package]] name = "path-slash" @@ -2198,7 +2208,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.15.22" dependencies = [ "anyhow", "argfile", @@ -2255,7 +2265,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anstyle", "memchr", @@ -2264,8 +2274,9 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.3" +version = "0.0.5" dependencies = [ + "char_str", "filetime", "glob", "globset", @@ -2276,7 +2287,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anstyle", "arc-swap", @@ -2319,7 +2330,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "is-macro", @@ -2329,7 +2340,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.3" +version = "0.0.5" dependencies = [ "drop_bomb", "ruff_cache", @@ -2344,7 +2355,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -2365,7 +2376,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "ruff_macros", @@ -2374,7 +2385,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.20" +version = "0.15.22" dependencies = [ "aho-corasick", "anyhow", @@ -2433,7 +2444,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.3" +version = "0.0.5" dependencies = [ "heck", "itertools 0.15.0", @@ -2446,7 +2457,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.3" +version = "0.0.5" dependencies = [ "regex", "ruff_python_ast", @@ -2459,17 +2470,16 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", - "itertools 0.15.0", "rand 0.10.1", "ruff_diagnostics", "ruff_source_file", @@ -2482,18 +2492,19 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.3" +version = "0.0.5" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.3" +version = "0.0.5" dependencies = [ "aho-corasick", "arrayvec", "bitflags", + "char_str", "compact_str", "get-size2", "is-macro", @@ -2512,7 +2523,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -2523,7 +2534,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -2551,7 +2562,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "ruff_diagnostics", @@ -2564,7 +2575,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -2574,7 +2585,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", "icu_properties", @@ -2584,11 +2595,11 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", "bstr", - "compact_str", + "drop_bomb", "get-size2", "memchr", "ruff_python_ast", @@ -2604,7 +2615,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", "is-macro", @@ -2622,7 +2633,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", "unicode-ident", @@ -2630,7 +2641,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.3" +version = "0.0.5" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -2641,7 +2652,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "ruff_db", @@ -2652,7 +2663,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "crossbeam", @@ -2690,7 +2701,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "memchr", @@ -2700,7 +2711,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.3" +version = "0.0.5" dependencies = [ "get-size2", "serde", @@ -2708,7 +2719,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "colored", @@ -2772,7 +2783,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2789,9 +2800,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbaab832e2ea754afda4a738f987dd1e8bd30c9e5d8c981ee6a3934386095e2" +checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" dependencies = [ "boxcar", "compact_str", @@ -2816,20 +2827,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6872462ac73d39969a836273c24163e6a26a4e08f5114fcd80e25af30ea9c6" +checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" [[package]] name = "salsa-macros" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc78ffaf65b1a9175818592c5130aa10b1bb245a905722fd4db87cea8a8457" +checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" dependencies = [ "proc-macro2", "quote", "syn", - "synstructure", ] [[package]] @@ -3096,7 +3106,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3106,7 +3116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3399,7 +3409,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ordermap", "ruff_db", @@ -3446,7 +3456,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "camino", @@ -3478,9 +3488,10 @@ dependencies = [ "crossbeam", "get-size2", "globset", - "ignore", + "memchr", "notify", "ordermap", + "parking_lot", "pep440_rs", "rayon", "regex", @@ -3513,10 +3524,11 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", "bitvec", + "char_str", "get-size2", "hashbrown 0.17.1", "itertools 0.15.0", @@ -3542,9 +3554,10 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.3" +version = "0.0.5" dependencies = [ "bitflags", + "char_str", "compact_str", "drop_bomb", "get-size2", @@ -3577,6 +3590,7 @@ dependencies = [ "ty_module_resolver", "ty_python_core", "ty_site_packages", + "ty_static", "unicode-segmentation", ] @@ -3617,7 +3631,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.3" +version = "0.0.5" dependencies = [ "camino", "colored", @@ -3637,14 +3651,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.3" +version = "0.0.5" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.3" +version = "0.0.5" dependencies = [ "path-slash", "ruff_db", @@ -3950,7 +3964,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/basedpython/Cargo.toml b/crates/basedpython/Cargo.toml index 9d3ef7defc..2fbc869743 100644 --- a/crates/basedpython/Cargo.toml +++ b/crates/basedpython/Cargo.toml @@ -15,7 +15,7 @@ name = "basedpython" version = "0.0.1-a5" edition = "2024" -rust-version = "1.94" +rust-version = "1.95" publish = false # cargo-dist reads this crate (it drives the release version); it needs these # metadata keys, and the crate is its own workspace so it can't inherit them from diff --git a/crates/basedpython/src/by.rs b/crates/basedpython/src/by.rs index 3326bf3509..aa585b80ec 100644 --- a/crates/basedpython/src/by.rs +++ b/crates/basedpython/src/by.rs @@ -24,6 +24,17 @@ pub fn main() -> ExitStatus { run().unwrap_or_else(|error| { use io::Write; + // Exit "gracefully" on broken pipe errors. + // + // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 + if error.chain().any(|cause| { + cause + .downcast_ref::() + .is_some_and(|ioerr| ioerr.kind() == io::ErrorKind::BrokenPipe) + }) { + return ExitStatus::Success; + } + // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. let mut stderr = io::stderr().lock(); @@ -34,15 +45,6 @@ pub fn main() -> ExitStatus { // the configuration it is help to chain errors ("resolving configuration failed" -> // "failed to read file: subdir/pyproject.toml") for cause in error.chain() { - // Exit "gracefully" on broken pipe errors. - // - // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 - if let Some(ioerr) = cause.downcast_ref::() { - if ioerr.kind() == io::ErrorKind::BrokenPipe { - return ExitStatus::Success; - } - } - writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); } diff --git a/crates/by_override_patch/src/main.rs b/crates/by_override_patch/src/main.rs index 144c11a761..d5db3cfc36 100644 --- a/crates/by_override_patch/src/main.rs +++ b/crates/by_override_patch/src/main.rs @@ -102,7 +102,7 @@ fn mark_pass(stdlib_dir: &Path, typeshed_root: &Path) -> Result<(usize, usize)> let system = OsSystem::new(SystemPathBuf::from_path_buf_lossy(project_root.clone())); let root = SystemPathBuf::from_path_buf_lossy(project_root); let mut metadata = ProjectMetadata::new("typeshed-override", root); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(ty_project::metadata::options::EnvironmentOptions { typeshed: Some(ty_project::metadata::value::RelativePathBuf::cli( SystemPathBuf::from_path_buf_lossy(typeshed_root.to_path_buf()), diff --git a/crates/by_typeshed_patch/src/lib.rs b/crates/by_typeshed_patch/src/lib.rs index 25b30fffe9..4f3694bfa5 100644 --- a/crates/by_typeshed_patch/src/lib.rs +++ b/crates/by_typeshed_patch/src/lib.rs @@ -48,10 +48,7 @@ pub struct Edit { pub fn all_patches() -> Vec> { // patches are added here as upstream syncs surface concrete drift. each // entry must have a corresponding module in `src/patches/` with tests - vec![ - Box::new(patches::mapping::MappingKeyCovariance), - Box::new(patches::container_overlapping::ContainerMembershipOverlapping), - ] + vec![Box::new(patches::mapping::MappingKeyCovariance)] } /// registry of every post-conversion patch, applied in pass 3 after the pep 695 @@ -74,6 +71,10 @@ pub fn all_post_patches(root: &Path) -> Vec> { // form; runs first so later idiom patches (e.g. `any_to_dynamic`) still see // and normalise anything it introduces Box::new(patches::output_widening::OutputWidening), + // rewrites membership/lookup parameters over the converted names + // (`Element`, `Key`), so it cannot run in pass 1 alongside + // `MappingKeyCovariance` — both would edit `Mapping.__getitem__` + Box::new(patches::container_overlapping::ContainerMembershipOverlapping), Box::new(patches::cleanup::StripIgnoreComments), Box::new(patches::cleanup::BodylessStubs), Box::new(patches::dead_symbols::DeleteDeadSymbols), diff --git a/crates/by_typeshed_patch/src/patches/container_overlapping.rs b/crates/by_typeshed_patch/src/patches/container_overlapping.rs index 972be402e9..415e76cc92 100644 --- a/crates/by_typeshed_patch/src/patches/container_overlapping.rs +++ b/crates/by_typeshed_patch/src/patches/container_overlapping.rs @@ -19,9 +19,9 @@ //! override compatibility (a subclass may override with `Key` or the bare upper //! bound); only the call site applies the overlap admissibility check //! -//! this patch operates on the already-pep695-converted committed tree (run via -//! `by_typeshed_patch --skip-conversion`), matching the nice type-parameter -//! names (`Element`, `Key`) rather than the legacy `_T_co`/`_KT_co` form +//! this patch matches the nice type-parameter names (`Element`, `Key`) rather +//! than the legacy `_T_co`/`_KT_co` form, so it is registered as a +//! post-conversion patch (pass 3), where each patch gets its own re-parse use std::path::Path; diff --git a/crates/mdtest/Cargo.toml b/crates/mdtest/Cargo.toml index 14b57d3d78..75e1bbbcbd 100644 --- a/crates/mdtest/Cargo.toml +++ b/crates/mdtest/Cargo.toml @@ -38,6 +38,7 @@ similar = { workspace = true } smallvec = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } +toml_parser = { workspace = true } tracing = { workspace = true } [lints] diff --git a/crates/mdtest/src/assertion.rs b/crates/mdtest/src/assertion.rs index be3318cb69..2c4dd279d4 100644 --- a/crates/mdtest/src/assertion.rs +++ b/crates/mdtest/src/assertion.rs @@ -1,4 +1,4 @@ -//! Parse type and type-error assertions in Python comment form. +//! Parse inline diagnostic assertions from comments. //! //! Parses comments of the form `# revealed: SomeType` and `# error: 8 [rule-code] "message text"`. //! In the latter case, the `8` is a column number, and `"message text"` asserts that the full @@ -35,15 +35,21 @@ //! ``` use ruff_db::parsed::ParsedModuleRef; -use ruff_python_ast::token::Token; use ruff_python_trivia::{CommentRanges, Cursor}; use ruff_source_file::{LineIndex, OneIndexed}; use ruff_text_size::{Ranged, TextRange, TextSize}; use smallvec::SmallVec; use std::str::FromStr; +use toml_parser::lexer::TokenKind; use crate::RunOptions; +#[derive(Clone, Copy)] +pub(crate) enum AssertionSource<'a> { + Python(&'a ParsedModuleRef), + Toml, +} + /// Diagnostic assertion comments in a single embedded file. #[derive(Debug)] pub(crate) struct InlineFileAssertions<'s> { @@ -53,15 +59,49 @@ pub(crate) struct InlineFileAssertions<'s> { impl<'s> InlineFileAssertions<'s> { pub(crate) fn from_file( source: &'s str, - parsed: &ParsedModuleRef, + assertion_source: AssertionSource<'_>, file_index: &LineIndex, ) -> Self { - let mut by_line = Vec::new(); - let mut file_assertions = UnparsedAssertionsIter { - tokens: parsed.tokens().iter(), - source, + match assertion_source { + AssertionSource::Python(parsed) => Self::from_comment_ranges( + source, + parsed + .tokens() + .iter() + .filter(|token| token.kind().is_comment()) + .map(Ranged::range), + file_index, + ), + AssertionSource::Toml => Self::from_comment_ranges( + source, + toml_parser::Source::new(source) + .lex() + .filter(|token| token.kind() == TokenKind::Comment) + .map(|token| { + let span = token.span(); + TextRange::new( + TextSize::try_from(span.start()).unwrap(), + TextSize::try_from(span.end()).unwrap(), + ) + }), + file_index, + ), } - .peekable(); + } + + fn from_comment_ranges( + source: &'s str, + comment_ranges: impl Iterator, + file_index: &LineIndex, + ) -> Self { + let mut by_line = Vec::new(); + let mut file_assertions = comment_ranges + .filter_map(|range| { + let comment_text = &source[range]; + UnparsedAssertion::from_comment(comment_text) + .map(|assertion| AssertionWithRange(assertion, range)) + }) + .peekable(); while let Some(ranged_assertion) = file_assertions.next() { let mut collector = AssertionVec::new(); @@ -150,29 +190,6 @@ impl<'s> IntoIterator for InlineFileAssertions<'s> { } } -struct UnparsedAssertionsIter<'a, 's> { - source: &'s str, - tokens: std::slice::Iter<'a, Token>, -} - -impl<'s> Iterator for UnparsedAssertionsIter<'_, 's> { - type Item = AssertionWithRange<'s>; - - fn next(&mut self) -> Option { - loop { - let token = self.tokens.next()?; - if !token.kind().is_comment() { - continue; - } - - let comment_text = &self.source[token.range()]; - if let Some(assertion) = UnparsedAssertion::from_comment(comment_text) { - return Some(AssertionWithRange(assertion, token.range())); - } - } - } -} - /// An [`UnparsedAssertion`] with the [`TextRange`] of its original inline comment. #[derive(Debug)] struct AssertionWithRange<'a>(UnparsedAssertion<'a>, TextRange); @@ -530,7 +547,18 @@ mod tests { db.write_file("/src/test.py", source).unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap(); let parsed = parsed_module(&db, file).load(&db); - InlineFileAssertions::from_file(source, &parsed, &line_index(&db, file)) + InlineFileAssertions::from_file( + source, + AssertionSource::Python(&parsed), + &line_index(&db, file), + ) + } + + fn get_toml_assertions(source: &str) -> InlineFileAssertions<'_> { + let mut db = TestDb::setup(); + db.write_file("/src/ruff.toml", source).unwrap(); + let file = system_path_to_file(&db, "/src/ruff.toml").unwrap(); + InlineFileAssertions::from_file(source, AssertionSource::Toml, &line_index(&db, file)) } fn into_vec(assertions: InlineFileAssertions<'_>) -> Vec> { @@ -581,6 +609,27 @@ mod tests { assert_eq!(format!("{assert}"), "error: "); } + #[test] + fn toml_comments() { + let source = dedent( + r##" + first = "# error: [not-a-comment]" + second = "value" # error: [rule-codes-in-selectors] + "##, + ); + let assertions = get_toml_assertions(&source); + + let [line] = &into_vec(assertions)[..] else { + panic!("expected one line"); + }; + + assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2)); + let [assertion] = &line.assertions[..] else { + panic!("expected one assertion"); + }; + assert_eq!(format!("{assertion}"), "error: [rule-codes-in-selectors]"); + } + #[test] fn prior_line() { let source = dedent( diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index a76b97d51a..a1d345dd39 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -538,7 +538,15 @@ pub fn create_diagnostic_snapshot<'d, C>( writeln!(snapshot, "---").unwrap(); writeln!(snapshot).unwrap(); - writeln!(snapshot, "# Python source files").unwrap(); + let source_heading = if test + .files() + .all(|file| matches!(file.lang, "py" | "python" | "pyi" | "ipynb")) + { + "Python source files" + } else { + "Source files" + }; + writeln!(snapshot, "# {source_heading}").unwrap(); writeln!(snapshot).unwrap(); for file in test.files() { writeln!(snapshot, "## {}", file.relative_path()).unwrap(); diff --git a/crates/mdtest/src/matcher.rs b/crates/mdtest/src/matcher.rs index ff4e31a763..8f5eb6a3d0 100644 --- a/crates/mdtest/src/matcher.rs +++ b/crates/mdtest/src/matcher.rs @@ -17,7 +17,9 @@ use ruff_source_file::{LineIndex, OneIndexed}; use smallvec::SmallVec; use crate::RunOptions; -use crate::assertion::{InlineFileAssertions, LineAssertions, ParsedAssertion, UnparsedAssertion}; +use crate::assertion::{ + AssertionSource, InlineFileAssertions, LineAssertions, ParsedAssertion, UnparsedAssertion, +}; use crate::diagnostic::SortedDiagnostics; #[derive(Debug, Default)] @@ -97,25 +99,34 @@ pub fn match_file( // Parse assertions from comments in the file, and get diagnostics from the file; both // ordered by line number. let source = source_text(db, file); - let parsed = parsed_module(db, file).load(db); let line_index = line_index(db, file); - let assertions = InlineFileAssertions::from_file(&source, &parsed, &line_index); - - // Sort diagnostics according to the line number of the starting offset of the token in which the diagnostic appears. - // - // This can be different to the line number of the starting offset of the diagnostic range! - // For example, if the diagnostic is a syntax error inside a stringized annotation, - // the syntax error's range will likely point to a sub-range of the string literal, - // which will make the error unmatchable by mdtest unless we look at the token in which - // the diagnostic occurs (the string-literal) and use the token start as the basis for - // the line number. - let diagnostics = SortedDiagnostics::new(diagnostics, &|diagnostic_range| { - let token_start = parsed - .tokens() - .token_range(diagnostic_range.start()) - .start(); - line_index.line_index(token_start) - }); + let (assertions, diagnostics) = if file.path(db).extension() == Some("toml") { + let assertions = + InlineFileAssertions::from_file(source.as_str(), AssertionSource::Toml, &line_index); + let diagnostics = SortedDiagnostics::new(diagnostics, &|diagnostic_range| { + line_index.line_index(diagnostic_range.start()) + }); + (assertions, diagnostics) + } else { + let parsed = parsed_module(db, file).load(db); + let assertions = InlineFileAssertions::from_file( + source.as_str(), + AssertionSource::Python(&parsed), + &line_index, + ); + + // Sort diagnostics according to the line number of the starting offset of the token in + // which the diagnostic appears. This can differ from the line containing the start of the + // diagnostic range, for example for syntax errors inside stringized annotations. + let diagnostics = SortedDiagnostics::new(diagnostics, &|diagnostic_range| { + let token_start = parsed + .tokens() + .token_range(diagnostic_range.start()) + .start(); + line_index.line_index(token_start) + }); + (assertions, diagnostics) + }; let mut line_diagnostics = diagnostics.iter_lines(); @@ -262,31 +273,13 @@ impl UnmatchedWithColumn for &Diagnostic { /// Discard `@Todo`-type metadata from expected types, which is not available /// when running in release mode. -/// -/// Some `@Todo` variants (like `@Todo(StarredExpression)` and `@Todo(typing.Unpack)`) -/// are hardcoded enum variants that always display their message, so we preserve those. fn discard_todo_metadata(ty: &str) -> Cow<'_, str> { #[cfg(not(debug_assertions))] { - /// `@Todo` variants that are hardcoded and always display their message, - /// even in release mode. - const PRESERVED_TODO_VARIANTS: &[&str] = &[ - "@Todo(StarredExpression)", - "@Todo(typing.Unpack)", - "@Todo(TypeVarTuple)", - ]; - static TODO_METADATA_REGEX: LazyLock = LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap()); - TODO_METADATA_REGEX.replace_all(ty, |caps: ®ex::Captures| { - let matched = caps.get(0).unwrap().as_str(); - if PRESERVED_TODO_VARIANTS.contains(&matched) { - matched.to_string() - } else { - "@Todo".to_string() - } - }) + TODO_METADATA_REGEX.replace_all(ty, "@Todo") } #[cfg(debug_assertions)] diff --git a/crates/mdtest/src/parser.rs b/crates/mdtest/src/parser.rs index d5b3cd60eb..f5b9241596 100644 --- a/crates/mdtest/src/parser.rs +++ b/crates/mdtest/src/parser.rs @@ -393,14 +393,13 @@ impl EmbeddedFilePath<'_> { /// A single file embedded in a [`Section`] as a fenced code block. /// /// Currently must be a Python file (`py` language), a type stub (`pyi`), a Jupyter notebook -/// (`ipynb`) or a [typeshed `VERSIONS`] file. +/// (`ipynb`), an explicitly named TOML file, or a [typeshed `VERSIONS`] file. /// -/// TOML configuration blocks are also supported, but are not stored as `EmbeddedFile`s. In the -/// future we plan to support `pth` files as well. +/// Unnamed TOML blocks configure the test and are not stored as `EmbeddedFile`s. In the future we +/// plan to support `pth` files as well. /// -/// A Python embedded file makes its containing [`Section`] into a [`MarkdownTest`], and will be -/// type-checked and searched for inline-comment assertions to match against the diagnostics from -/// type checking. +/// A checkable embedded file makes its containing [`Section`] into a [`MarkdownTest`] and is +/// searched for inline-comment assertions to match against diagnostics. /// /// [typeshed `VERSIONS`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18 #[derive(Debug)] @@ -410,7 +409,7 @@ pub struct EmbeddedFile<'s> { pub lang: &'s str, pub code: Cow<'s, str>, /// The checkable code blocks - pub python_code_blocks: Vec>, + pub code_blocks: Vec>, } impl EmbeddedFile<'_> { @@ -426,7 +425,7 @@ impl EmbeddedFile<'_> { let start_offset = existing_code.text_len(); existing_code.push_str(new_code); - self.python_code_blocks.push(CodeBlock { + self.code_blocks.push(CodeBlock { backticks: backtick_offsets, embedded_start_offset: start_offset, inline_snapshot_block: None, @@ -452,7 +451,7 @@ impl EmbeddedFile<'_> { pub(crate) fn is_checkable(&self) -> bool { matches!( self.lang, - "py" | "python" | "pyi" | "ipynb" | "by" | "byi" | "bython" | "basedpython" + "py" | "python" | "pyi" | "ipynb" | "toml" | "by" | "byi" | "bython" | "basedpython" ) } } @@ -714,12 +713,29 @@ where self.skip_non_newline_whitespace(); + let metadata = self.consume_until(|c| c == '\n').unwrap_or_default().trim(); + if !self.cursor.eat_char('\n') { bail!( "Trailing code-block metadata is not supported. Only the code block language can be specified." ); } + let ignore = if metadata.is_empty() { + false + } else if let Some(attributes) = metadata + .strip_prefix('{') + .and_then(|metadata| metadata.strip_suffix('}')) + { + attributes + .split_ascii_whitespace() + .any(|attribute| attribute == r#"data-mdtest="ignore""#) + } else { + bail!( + "Trailing code-block metadata must use the `{{...}}` attribute-list syntax." + ); + }; + if let Some(position) = memchr::memmem::find(self.cursor.as_bytes(), CODE_BLOCK_END) { @@ -735,6 +751,7 @@ where self.process_code_block( lang, code, + ignore, BacktickOffsets(TextRange::new( backtick_offset_start, backtick_offset_end, @@ -825,13 +842,18 @@ where &mut self, lang: &'s str, code: &'s str, + ignore: bool, backtick_offsets: BacktickOffsets, ) -> anyhow::Result<()> { // We never pop the implicit root section. let section = self.stack.top(); let test_name = self.sections[section].title; - if lang == "toml" { + if ignore { + return Ok(()); + } + + if lang == "toml" && self.explicit_path.is_none() { return self.process_config_block(code); } @@ -908,7 +930,7 @@ where section, lang, code: Cow::Borrowed(code), - python_code_blocks: vec![CodeBlock { + code_blocks: vec![CodeBlock { backticks: backtick_offsets, embedded_start_offset: TextSize::new(0), inline_snapshot_block: None, @@ -948,7 +970,7 @@ where fn current_section_has_merged_snippets(&self) -> bool { self.current_section_files .values() - .any(|id| self.files[*id].python_code_blocks.len() > 1) + .any(|id| self.files[*id].code_blocks.len() > 1) } fn process_config_block(&mut self, code: &str) -> anyhow::Result<()> { @@ -977,7 +999,7 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a Python code block, but section has no files." + "`snapshot` code block on line {backtick_start} must follow a checkable code block, but section has no files." ); }; @@ -987,12 +1009,12 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a `python` code block in the same section but it follows a `{}` block.", + "`snapshot` code block on line {backtick_start} must follow a checkable code block in the same section but it follows a `{}` block.", file.lang ); } - let code_block = file.python_code_blocks.last_mut().unwrap(); + let code_block = file.code_blocks.last_mut().unwrap(); if let Some(existing_block) = &code_block.inline_snapshot_block { let code_block_start = line_number(code_block.embedded_start_offset(), self.source); @@ -1000,7 +1022,7 @@ where let existing_start = line_number(existing_block.range.start(), self.source); bail!( - "Python code block on line `{code_block_start}` has more than one `snapshot` block: first on line {existing_start} and another on line {backtick_start}.", + "Code block on line `{code_block_start}` has more than one `snapshot` block: first on line {existing_start} and another on line {backtick_start}.", ); } @@ -1265,6 +1287,32 @@ mod tests { assert_eq!(file.code, "{}"); } + #[test] + fn explicitly_named_toml_file() { + let source = dedent( + r#" + `ruff.toml`: + + ```toml + lint.select = ["F401"] + ``` + "#, + ); + let mf = parse("file.md", &source).unwrap(); + + let [test] = &mf.tests().collect::>()[..] else { + panic!("expected one test"); + }; + + let [file] = test.files().collect::>()[..] else { + panic!("expected one file"); + }; + + assert_eq!(file.path, EmbeddedFilePath::Explicit("ruff.toml")); + assert_eq!(file.lang, "toml"); + assert_eq!(file.code, r#"lint.select = ["F401"]"#); + } + #[test] fn multiple_tests() { let source = dedent( @@ -1663,6 +1711,33 @@ mod tests { assert_eq!(file.code, "x = 1"); } + #[test] + fn ignores_python_blocks_with_ignore_metadata() { + let source = dedent( + r#" + # Example + + ```python {.example data-mdtest="ignore" title="Ignored example"} + x: int = "wrong, but not checked" + ``` + + ```py + x = 1 + ``` + "#, + ); + + let mf = parse("file.md", &source).unwrap(); + let [test] = &mf.tests().collect::>()[..] else { + panic!("expected one test"); + }; + let [file] = test.files().collect::>()[..] else { + panic!("expected one file"); + }; + + assert_eq!(file.code, "x = 1"); + } + #[test] fn mismatching_lang() { let source = dedent( @@ -2148,7 +2223,7 @@ mod tests { } #[test] - fn config_no_longer_allowed() { + fn unbraced_metadata_not_allowed() { let source = dedent( " ```py foo=bar @@ -2159,7 +2234,7 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Trailing code-block metadata is not supported. Only the code block language can be specified." + "Trailing code-block metadata must use the `{...}` attribute-list syntax." ); } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index ef6d675db1..7f6d3eb74d 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.20" +version = "0.15.22" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } @@ -60,7 +60,7 @@ itertools = { workspace = true } jiff = { workspace = true } log = { workspace = true } notify = { workspace = true } -path-absolutize = { workspace = true, features = ["once_cell_cache"] } +path-absolutize = { workspace = true, features = ["fixed_workdir"] } rayon = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index 51d723d72f..f9aa90fe1b 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.15.20. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff). +This is version 0.15.22. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs index 5a80e4f5ac..d363801ccd 100644 --- a/crates/ruff/src/commands/check.rs +++ b/crates/ruff/src/commands/check.rs @@ -50,7 +50,8 @@ pub(crate) fn check( if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { matches!( SourceType::from(path), - SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + SourceType::Python(_) + | SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) ) } else { true diff --git a/crates/ruff/src/commands/config.rs b/crates/ruff/src/commands/config.rs index f6c0054839..751e15876c 100644 --- a/crates/ruff/src/commands/config.rs +++ b/crates/ruff/src/commands/config.rs @@ -16,7 +16,7 @@ pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> { } HelpFormat::Json => { - println!("{}", &serde_json::to_string_pretty(&metadata)?); + println!("{}", serde_json::to_string_pretty(&metadata)?); } } } @@ -30,7 +30,7 @@ pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> { } HelpFormat::Json => { - println!("{}", &serde_json::to_string_pretty(&entry)?); + println!("{}", serde_json::to_string_pretty(&entry)?); } }, }, diff --git a/crates/ruff/src/commands/show_files.rs b/crates/ruff/src/commands/show_files.rs index 22826dbd0d..9ac22700d2 100644 --- a/crates/ruff/src/commands/show_files.rs +++ b/crates/ruff/src/commands/show_files.rs @@ -25,7 +25,8 @@ pub(crate) fn show_files( if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { matches!( SourceType::from(path), - SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + SourceType::Python(_) + | SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) ) } else { true diff --git a/crates/ruff/src/commands/version.rs b/crates/ruff/src/commands/version.rs index 3361071206..d750e90815 100644 --- a/crates/ruff/src/commands/version.rs +++ b/crates/ruff/src/commands/version.rs @@ -11,7 +11,7 @@ pub(crate) fn version(output_format: HelpFormat) -> Result<()> { match output_format { HelpFormat::Text => { - writeln!(stdout, "ruff {}", &version_info)?; + writeln!(stdout, "ruff {version_info}")?; } HelpFormat::Json => { serde_json::to_writer_pretty(stdout, &version_info)?; diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 8a6535a009..634ad80b1c 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -14,10 +14,10 @@ use ruff_db::diagnostic::Diagnostic; use ruff_linter::codes::Rule; use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only}; use ruff_linter::package::PackageRoot; -use ruff_linter::pyproject_toml::lint_pyproject_toml; use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::{LinterSettings, flags}; -use ruff_linter::source_kind::{SourceError, SourceKind}; +use ruff_linter::source_kind::{SourceError, SourceKind, SourceKindDiff}; +use ruff_linter::toml::{TomlFixerResult, lint_fix_toml, lint_toml}; use ruff_linter::{IOError, Violation, fs}; use ruff_notebook::{NotebookError, NotebookIndex}; use ruff_python_ast::{SourceType, TomlSourceType}; @@ -213,11 +213,11 @@ pub(crate) fn lint_path( debug!("Checking: {}", path.display()); let source_type = match settings.extension.get_source_type(path) { - SourceType::Toml(TomlSourceType::Pyproject) => { - let diagnostics = if settings + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + let (diagnostics, fixed) = if settings .rules .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + .any(|rule_code| rule_code.lint_source().is_toml()) { let contents = match std::fs::read_to_string(path).map_err(SourceError::from) { Ok(contents) => contents, @@ -225,14 +225,47 @@ pub(crate) fn lint_path( return Ok(Diagnostics::from_source_error(&err, Some(path), settings)); } }; - let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); - lint_pyproject_toml(&source_file, settings) + if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { + let TomlFixerResult { + diagnostics, + transformed, + fixed, + } = lint_fix_toml(path, &contents, settings, source_type, unsafe_fixes); + + if !fixed.is_empty() { + match fix_mode { + flags::FixMode::Apply => { + File::create(path)?.write_all(transformed.as_bytes())?; + } + flags::FixMode::Diff => { + write!( + &mut io::stdout().lock(), + "{}", + SourceKindDiff::from_text( + &contents, + transformed.as_ref(), + Some(path), + ) + )?; + } + flags::FixMode::Generate => {} + } + } + + (diagnostics, fixed) + } else { + ( + lint_toml(path, &contents, settings, source_type), + FixTable::default(), + ) + } } else { - vec![] + (vec![], FixTable::default()) }; return Ok(Diagnostics { inner: diagnostics, - ..Diagnostics::default() + fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]), + notebook_indexes: FxHashMap::default(), }); } SourceType::Toml(_) | SourceType::Markdown => return Ok(Diagnostics::default()), @@ -359,28 +392,63 @@ pub(crate) fn lint_stdin( .map(|path| settings.linter.extension.get_source_type(path)) .unwrap_or_default() { - SourceType::Toml(source_type) if source_type.is_pyproject() => { + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { if !settings .linter .rules .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + .any(|rule_code| rule_code.lint_source().is_toml()) { return Ok(Diagnostics::default()); } let path = path.unwrap(); - let source_file = - SourceFileBuilder::new(path.to_string_lossy(), contents.clone()).finish(); - match fix_mode { - flags::FixMode::Diff | flags::FixMode::Generate => {} - flags::FixMode::Apply => write!(&mut io::stdout().lock(), "{contents}")?, - } + let (diagnostics, fixed) = + if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { + let TomlFixerResult { + diagnostics, + transformed, + fixed, + } = lint_fix_toml( + path, + &contents, + &settings.linter, + source_type, + settings.unsafe_fixes, + ); + + match fix_mode { + flags::FixMode::Apply => { + write!(&mut io::stdout().lock(), "{transformed}")?; + } + flags::FixMode::Diff => { + if !fixed.is_empty() { + write!( + &mut io::stdout().lock(), + "{}", + SourceKindDiff::from_text( + &contents, + transformed.as_ref(), + Some(path), + ) + )?; + } + } + flags::FixMode::Generate => {} + } + + (diagnostics, fixed) + } else { + ( + lint_toml(path, &contents, &settings.linter, source_type), + FixTable::default(), + ) + }; return Ok(Diagnostics { - inner: lint_pyproject_toml(&source_file, &settings.linter), - fixed: FixMap::from_iter([(fs::relativize_path(path), FixTable::default())]), + inner: diagnostics, + fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]), notebook_indexes: FxHashMap::default(), }); } diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 80ca84b982..53c0c901f0 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -413,32 +413,27 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result { - let Some(change_kind) = change_detected(&event?) else { - continue; - }; - - if matches!(change_kind, ChangeKind::Configuration) { - pyproject_config = - resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; - } - Printer::clear_screen()?; - printer.write_to_user("File change detected...\n"); - - let diagnostics = commands::check::check( - &files, - &pyproject_config, - &config_arguments, - cache.into(), - noqa.into(), - fix_mode, - unsafe_fixes, - )?; - printer.write_continuously(&mut writer, &diagnostics, preview)?; - } - Err(err) => return Err(err.into()), + let Some(change_kind) = change_detected(&rx.recv()??) else { + continue; + }; + + if matches!(change_kind, ChangeKind::Configuration) { + pyproject_config = + resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; } + Printer::clear_screen()?; + printer.write_to_user("File change detected...\n"); + + let diagnostics = commands::check::check( + &files, + &pyproject_config, + &config_arguments, + cache.into(), + noqa.into(), + fix_mode, + unsafe_fixes, + )?; + printer.write_continuously(&mut writer, &diagnostics, preview)?; } } else { // Generate lint violations. diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 34a12ee0a4..c4f3e19c32 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -468,7 +468,6 @@ ignore = ["D203", "D212"] All checks passed! ----- stderr ----- - warning: No Python files found under the given path(s) "); Ok(()) @@ -3720,6 +3719,48 @@ def func(t: _T) -> _T: ); } +/// Test that `noqa` comments with rule codes +/// 1. Get replaced with Ruff-specific suppression comments (RUF105) +/// 2. Use human-readable rule names instead of codes (RUF106) +#[test] +fn noqa_comments_to_human_readable_ruff_ignores() -> Result<()> { + let fixture = CliTest::new()?; + let source = "# ruff: noqa: F401 +import os + +def foo(): + value = 1 # noqa: F841 +"; + + assert_cmd_snapshot!( + fixture + .check_command() + .args([ + "--select=F401,F841,RUF105,RUF106", + "--stdin-filename=test.py", + "--fix", + "--preview", + "-", + ]) + .pass_stdin(source), + @" + success: true + exit_code: 0 + ----- stdout ----- + # ruff:file-ignore[unused-import] + import os + + def foo(): + value = 1 # ruff:ignore[unused-variable] + + ----- stderr ----- + Found 4 errors (4 fixed, 0 remaining). + ", + ); + + Ok(()) +} + /// Test that we do not rename two different type parameters to the same name /// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would /// introduce invalid syntax) @@ -5150,3 +5191,42 @@ fn preview_default_rules() -> Result<()> { ); Ok(()) } + +#[test] +fn ruff_toml_is_linted() -> Result<()> { + let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; + + assert_cmd_snapshot!( + test.command().args([ + "check", + "--no-cache", + "--isolated", + "--preview", + "--select", + "RUF201", + ]), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + rule-codes-in-selectors: [*] Rule code used instead of name in `lint.select` + --> ruff.toml:1:17 + | + 1 | lint.select = ["F401"] + | ^^^^ + | + help: Replace rule code with `unused-import` + | + - lint.select = ["F401"] + 1 + lint.select = ["unused-import"] + | + + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "#, + ); + + Ok(()) +} diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index 321633fcc8..665c1c5939 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index 06dda6ee21..dd518b2d0a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -63,6 +63,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 27f4cbf3ed..1ebff9b713 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -65,6 +65,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", "*.md", ] file_resolver.extend_include = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 619b1f2cb9..bcb5d21ab5 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -65,6 +65,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 02dea957b1..d8c55c92f8 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -62,6 +62,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index e93c31ef81..f327185913 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -63,6 +63,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 0b78d0febf..1c5dd6daaa 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 3a9a2ec274..300ba580d2 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index 64c785b448..1e34f04ad6 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 5b7c112749..ca721fe339 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -58,6 +58,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index e3dd7d962e..655ac52cab 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -58,6 +58,8 @@ file_resolver.include = [ "*.byi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 67e5988adf..61568825af 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -1818,7 +1818,7 @@ fn check_input_from_argfile() -> Result<()> { )?; // Generate the args with the argfile notation - let argfile = format!("@{}", &input_file_path.display()); + let argfile = format!("@{}", input_file_path.display()); let mut cmd = RuffCheck::default().filename(argfile.as_ref()).build(); insta::with_settings!({filters => vec![ (file_a_path.display().to_string().as_str(), "/path/to/a.py"), diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index c92ae41a1b..d40beddeec 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs index 3785ab6749..85396c0a30 100644 --- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs +++ b/crates/ruff_annotate_snippets/src/renderer/display_list.rs @@ -1928,31 +1928,19 @@ pub(super) fn fmt_with_hyperlink<'a, T>( where T: std::fmt::Display + 'a, { - struct FmtHyperlink<'a, T> { - content: T, - url: Option<&'a str>, - } - - impl std::fmt::Display for FmtHyperlink<'_, T> - where - T: std::fmt::Display, - { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(url) = self.url { - write!(f, "\x1B]8;;{url}\x1B\\")?; - } + let url = if stylesheet.hyperlink { url } else { None }; - self.content.fmt(f)?; + fmt::from_fn(move |f| { + if let Some(url) = url { + write!(f, "\x1B]8;;{url}\x1B\\")?; + } - if self.url.is_some() { - f.write_str("\x1B]8;;\x1B\\")?; - } + content.fmt(f)?; - Ok(()) + if url.is_some() { + f.write_str("\x1B]8;;\x1B\\")?; } - } - - let url = if stylesheet.hyperlink { url } else { None }; - FmtHyperlink { content, url } + Ok(()) + }) } diff --git a/crates/ruff_benchmark/benches/module_resolution.rs b/crates/ruff_benchmark/benches/module_resolution.rs index 4cdbfddb51..c33fb80371 100644 --- a/crates/ruff_benchmark/benches/module_resolution.rs +++ b/crates/ruff_benchmark/benches/module_resolution.rs @@ -62,7 +62,7 @@ fn setup_case(n: usize) -> Case { fs.write_file_all(&importing_path, "").unwrap(); let mut metadata = ProjectMetadata::discover(SystemPath::new("/src"), &system).unwrap(); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(SupportedPythonVersion::Py312)), extra_paths: Some(extra_paths), diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 3d1eb1690d..7ce93a8a78 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -85,7 +85,7 @@ fn setup_tomllib_case() -> Case { let src_root = SystemPath::new("/src"); let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(SupportedPythonVersion::Py312)), ..EnvironmentOptions::default() @@ -163,13 +163,10 @@ fn benchmark_incremental(criterion: &mut Criterion) { fn incremental(case: &mut Case) { let Case { db, .. } = case; - db.apply_changes( - &[ChangeEvent::Changed { - path: case.file_path.clone(), - kind: ChangedKind::FileContent, - }], - None, - ); + db.apply_changes(&[ChangeEvent::Changed { + path: case.file_path.clone(), + kind: ChangedKind::FileContent, + }]); let result = db.check(); @@ -267,7 +264,7 @@ fn setup_micro_case_inner(code: &str, venv_path: Option<&Path>) -> Case { let src_root = SystemPath::new("/src"); let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(SupportedPythonVersion::Py312)), python, @@ -1416,6 +1413,51 @@ fn benchmark_typeis_narrowing(criterion: &mut Criterion) { }); } +/// Regression benchmark for . +/// +/// Non-terminal-call predicates must gate later narrowing. Keeping these scope-wide constraints in +/// an append-only tree avoids eagerly rewriting every live place state after each call. Exercise +/// unnarrowed, already-narrowed, and fixed-reachability bindings because each takes a different +/// path through reachability and narrowing evaluation. +fn benchmark_repeated_statement_calls(criterion: &mut Criterion) { + setup_rayon(); + + let cases = [ + ( + "ty_micro[repeated_statement_calls]", + String::from("def f() -> None:\n value = 'abc'\n"), + " value.upper()\n", + ), + ( + "ty_micro[repeated_statement_calls_pre_narrowed]", + String::from( + "def f(value: str | None) -> None:\n if value is None:\n return\n", + ), + " value.upper()\n", + ), + ( + "ty_micro[repeated_statement_calls_fixed_reachability]", + String::from("def f(value: str, flag: bool) -> None:\n if flag:\n"), + " value.upper()\n", + ), + ]; + + for (name, mut code, statement) in cases { + code.push_str(&statement.repeat(1_500)); + criterion.bench_function(name, |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); + } +} + /// Benchmarks solving many union-bearing upper bounds while inferring a generic call. /// /// Each callable argument places a distinct union upper bound on `T` through callable-parameter @@ -1678,6 +1720,43 @@ fn benchmark_fluid_many_widenings(criterion: &mut Criterion) { }); } +fn benchmark_many_invariant_typevars(criterion: &mut Criterion) { + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/3989. + let code = r#" +class Invariant[T]: + x: T + +def f[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]( + box1: Invariant[T1], + box2: Invariant[T2], + box3: Invariant[T3], + box4: Invariant[T4], + box5: Invariant[T5], + box6: Invariant[T6], + box7: Invariant[T7], + box8: Invariant[T8], + box9: Invariant[T9], + box10: Invariant[T10], +) -> None: ... + +x = Invariant[int]() +f(x, x, x, x, x, x, x, x, x, x) +"#; + + criterion.bench_function("ty_micro[many_invariant_typevars]", |b| { + b.iter_batched_ref( + || setup_micro_case(code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} fn benchmark_pydantic_core_schema_dict(criterion: &mut Criterion) { const NUM_CORE_SCHEMA_VARIANTS: usize = 24; @@ -1758,7 +1837,7 @@ impl<'a> ProjectBenchmark<'a> { let src_root = SystemPath::new("/"); let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(self.project.config.python_version)), python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))), @@ -1855,10 +1934,14 @@ fn attrs(criterion: &mut Criterion) { max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 102, + 104, ); bench_project(&benchmark, criterion); + + // Keep one real-world benchmark frozen to catch regressions from newly added inputs. + let frozen_benchmark = benchmark.freeze_inputs(); + bench_project_named(&frozen_benchmark, criterion, "attrs (frozen inputs)"); } fn anyio(criterion: &mut Criterion) { @@ -1893,10 +1976,6 @@ fn datetype(criterion: &mut Criterion) { ); bench_project(&benchmark, criterion); - - // Keep one cheap real-world benchmark frozen to catch regressions from newly added inputs. - let frozen_benchmark = benchmark.freeze_inputs(); - bench_project_named(&frozen_benchmark, criterion, "DateType (frozen inputs)"); } criterion_group!(check_file, benchmark_cold, benchmark_incremental); @@ -1929,11 +2008,13 @@ criterion_group!( benchmark_literal_equality_fallthrough_guarded_any, benchmark_literal_or_pattern_reachability, benchmark_typeis_narrowing, + benchmark_repeated_statement_calls, benchmark_factored_upper_bounds, benchmark_pandas_tdd, benchmark_recursive_typed_dict_union_contextual_inference, benchmark_invariant_generic_return_union, benchmark_invariant_generic_union_bound, + benchmark_many_invariant_typevars, benchmark_pydantic_core_schema_dict, benchmark_fluid_many_widenings, ); diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 61c1fd58e4..6d54fbb17b 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -43,7 +43,7 @@ impl<'a> Benchmark<'a> { let mut metadata = ProjectMetadata::discover(&root, &system).unwrap(); - metadata.apply_options(Options { + metadata.apply_override_options(Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(installed_project.config.python_version)), python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))), @@ -198,7 +198,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 16500, + 16617, ); static TANJUN: Benchmark = Benchmark::new( @@ -211,7 +211,7 @@ static TANJUN: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 110, + 124, ); static STATIC_FRAME: Benchmark = Benchmark::new( diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 42d2299e0f..933e85eae3 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -11,6 +11,7 @@ repository = { workspace = true } license = { workspace = true } [dependencies] +char_str = { workspace = true } filetime = { workspace = true } glob = { workspace = true } globset = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index 582b8013ba..2a8e9a65a2 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_cache). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index e371f4baba..e08606eb9e 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -7,6 +7,7 @@ use std::num::{ }; use std::path::{Path, PathBuf}; +use char_str::CharStr; use glob::Pattern; use itertools::Itertools; use regex::Regex; @@ -256,6 +257,13 @@ impl CacheKey for String { } } +impl CacheKey for CharStr { + #[inline] + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.as_str().cache_key(state); + } +} + impl CacheKey for Option { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index bd08540a43..757321d855 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index e901a90ee5..d755d0a8bd 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_db). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/src/diagnostic/stylesheet.rs b/crates/ruff_db/src/diagnostic/stylesheet.rs index f0e13d0cd4..ed4baf37b9 100644 --- a/crates/ruff_db/src/diagnostic/stylesheet.rs +++ b/crates/ruff_db/src/diagnostic/stylesheet.rs @@ -41,33 +41,21 @@ pub(super) fn fmt_with_hyperlink<'a, T>( where T: std::fmt::Display + 'a, { - struct FmtHyperlink<'a, T> { - content: T, - url: Option<&'a str>, - } - - impl std::fmt::Display for FmtHyperlink<'_, T> - where - T: std::fmt::Display, - { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if let Some(url) = self.url { - write!(f, "\x1B]8;;{url}\x1B\\")?; - } + let url = if stylesheet.hyperlink { url } else { None }; - self.content.fmt(f)?; + std::fmt::from_fn(move |f| { + if let Some(url) = url { + write!(f, "\x1B]8;;{url}\x1B\\")?; + } - if self.url.is_some() { - f.write_str("\x1B]8;;\x1B\\")?; - } + content.fmt(f)?; - Ok(()) + if url.is_some() { + f.write_str("\x1B]8;;\x1B\\")?; } - } - - let url = if stylesheet.hyperlink { url } else { None }; - FmtHyperlink { content, url } + Ok(()) + }) } #[derive(Clone, Debug)] diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 20da38d529..20dc3bcfaa 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -108,6 +108,19 @@ impl Files { /// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory. /// In these cases, a file with the appropriate [`FileStatus`] is returned. fn system(&self, db: &dyn Db, path: &SystemPath) -> File { + // All cache keys are normalized, absolute paths. However, an absolute path does not need + // to be fully normalized for this lookup: Camino's equality and hashing ignore redundant + // separators and `.` components, so `/foo/bar.py`, `/foo//bar.py`, and `/foo/./bar.py` + // all match the same cached `File`. A `..` component is not ignored, so a path such as + // `/foo/baz/../bar.py` misses the cache and falls through to full normalization. Since + // this fast path only returns an existing entry and never inserts one, it cannot create + // separate `File` identities for different spellings of the same path. + if path.is_absolute() + && let Some(file) = self.inner.system_by_path.get(path) + { + return *file; + } + let absolute = SystemPath::absolute(path, db.system().current_directory()); // DashMap's entry API requires an owned key. Avoid cloning it for cached paths. @@ -153,6 +166,13 @@ impl Files { /// Tries to look up the file for the given system path, returns `None` if no such file exists yet pub fn try_system(&self, db: &dyn Db, path: &SystemPath) -> Option { + // As in `system`, path equality normalizes redundant separators and `.`, but not `..`. + if path.is_absolute() + && let Some(file) = self.inner.system_by_path.get(path) + { + return Some(*file); + } + let absolute = SystemPath::absolute(path, db.system().current_directory()); self.inner .system_by_path @@ -344,10 +364,12 @@ pub struct File { /// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows /// or when the file has been deleted. #[default] + #[returns(copy)] pub permissions: Option, /// The path revision. A file or directory has changed if the revisions don't compare equal. #[default] + #[returns(copy)] pub revision: FileRevision, /// The status of the file. @@ -355,6 +377,7 @@ pub struct File { /// Salsa doesn't support deleting inputs. The only way to signal dependent queries that /// the file has been deleted is to change the status to `Deleted`. #[default] + #[returns(copy)] pub status: FileStatus, /// Overrides the result of [`source_text`](crate::source::source_text). @@ -709,9 +732,9 @@ mod tests { use crate::Db as _; use crate::file_revision::FileRevision; - use crate::files::{FileError, system_path_to_file, vendored_path_to_file}; + use crate::files::{File, FileError, system_path_to_file, vendored_path_to_file}; use crate::source::source_text; - use crate::system::DbWithWritableSystem as _; + use crate::system::{DbWithWritableSystem as _, SystemPath}; use crate::tests::TestDb; use crate::vendored::VendoredFileSystemBuilder; use zip::CompressionMethod; @@ -742,17 +765,27 @@ mod tests { #[test] fn system_normalize_paths() { - let db = TestDb::new(); + #[track_caller] + fn assert_normalized_path(db: &TestDb, path: &str, canonical: File) { + assert_eq!(system_path_to_file(db, path), Ok(canonical)); + assert_eq!( + db.files().try_system(db, SystemPath::new(path)), + Some(canonical) + ); + } - assert_eq!( - system_path_to_file(&db, "test.py"), - system_path_to_file(&db, "/test.py") - ); + let mut db = TestDb::new(); + db.write_file("/foo/bar.py", "x = 1").unwrap(); + db.write_file("/foo/baz/bar.py", "x = 2").unwrap(); - assert_eq!( - system_path_to_file(&db, "/root/.././test.py"), - system_path_to_file(&db, "/root/test.py") - ); + let canonical = system_path_to_file(&db, "/foo/bar.py").unwrap(); + assert_normalized_path(&db, "foo/bar.py", canonical); + assert_normalized_path(&db, "/foo//bar.py", canonical); + assert_normalized_path(&db, "/foo/./bar.py", canonical); + assert_normalized_path(&db, "/foo/baz/../bar.py", canonical); + + let distinct = system_path_to_file(&db, "/foo/baz/bar.py").unwrap(); + assert_ne!(canonical, distinct); } #[test] diff --git a/crates/ruff_db/src/files/file_root.rs b/crates/ruff_db/src/files/file_root.rs index 919bf152f8..db54fde524 100644 --- a/crates/ruff_db/src/files/file_root.rs +++ b/crates/ruff_db/src/files/file_root.rs @@ -18,6 +18,7 @@ pub struct FileRoot { pub path: Box, /// The kind of the root at the time of its creation. + #[returns(copy)] pub kind_at_time_of_creation: FileRootKind, } diff --git a/crates/ruff_db/src/panic.rs b/crates/ruff_db/src/panic.rs index 61faf91875..6d12e5bd0f 100644 --- a/crates/ruff_db/src/panic.rs +++ b/crates/ruff_db/src/panic.rs @@ -183,10 +183,11 @@ mod tests { fn no_backtrace_for_salsa_cancelled() { #[salsa::input] struct Input { + #[returns(copy)] value: u32, } - #[salsa::tracked] + #[salsa::tracked(returns(copy))] fn test_query(db: &dyn Database, input: Input) -> u32 { loop { // This should throw a cancelled error diff --git a/crates/ruff_db/src/source.rs b/crates/ruff_db/src/source.rs index 5a2d1b29f2..511a7e7fa6 100644 --- a/crates/ruff_db/src/source.rs +++ b/crates/ruff_db/src/source.rs @@ -12,7 +12,7 @@ use crate::files::{File, FilePath}; use crate::system::System; /// Reads the source text of a python text file (must be valid UTF8) or notebook. -#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)] pub fn source_text(db: &dyn Db, file: File) -> SourceText { let path = file.path(db); let _span = tracing::trace_span!("source_text", file = %path).entered(); @@ -202,7 +202,7 @@ pub enum SourceTextError { } /// Computes the [`LineIndex`] for `file`. -#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)] pub fn line_index(db: &dyn Db, file: File) -> LineIndex { let _span = tracing::trace_span!("line_index", ?file).entered(); diff --git a/crates/ruff_db/src/system/memory_fs.rs b/crates/ruff_db/src/system/memory_fs.rs index b8c469ca6e..bba5c6e783 100644 --- a/crates/ruff_db/src/system/memory_fs.rs +++ b/crates/ruff_db/src/system/memory_fs.rs @@ -13,8 +13,8 @@ use crate::system::{ }; use super::walk_directory::{ - DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, WalkDirectoryVisitor, - WalkDirectoryVisitorBuilder, WalkState, + DirectoryWalker, IgnoreIncremental, WalkDirectoryBuilder, WalkDirectoryConfiguration, + WalkDirectoryVisitor, WalkDirectoryVisitorBuilder, WalkState, }; /// File system that stores all content in memory. @@ -564,6 +564,20 @@ impl Iterator for ReadDirectory { impl FusedIterator for ReadDirectory {} +struct MemoryIgnoreIncremental { + ignore_hidden: bool, +} + +impl IgnoreIncremental for MemoryIgnoreIncremental { + fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool { + // This matches the semantics of the in-memory recursive + // directory traversal. That is, the only thing we care + // about filtering is hidden files. We let everything else + // through. + self.ignore_hidden && !is_directory && is_hidden(path) + } +} + /// Recursively walks a directory in the memory file system. #[derive(Debug)] struct MemoryWalker { @@ -592,12 +606,7 @@ impl MemoryWalker { } state - } else if ignore_hidden - && entry - .path - .file_name() - .is_some_and(|name| name.starts_with('.')) - { + } else if ignore_hidden && is_hidden(&entry.path) { WalkState::Skip } else { visitor.visit(Ok(entry)) @@ -702,6 +711,14 @@ impl DirectoryWalker for MemoryWalker { } } } + + fn incremental_matcher( + &self, + configuration: WalkDirectoryConfiguration, + ) -> Box { + let WalkDirectoryConfiguration { ignore_hidden, .. } = configuration; + Box::new(MemoryIgnoreIncremental { ignore_hidden }) + } } #[derive(Debug)] @@ -713,6 +730,10 @@ enum WalkerState { Nested { path: SystemPathBuf, depth: usize }, } +fn is_hidden(path: &SystemPath) -> bool { + path.file_name().is_some_and(|name| name.starts_with('.')) +} + #[cfg(test)] mod tests { use std::io::ErrorKind; diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index 337e017d13..d2f88941ef 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -1,7 +1,10 @@ #![allow(clippy::disallowed_methods)] +mod ignore; + +use self::ignore::IgnoreFiles; use super::walk_directory::{ - self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, + self, DirectoryWalker, IgnoreIncremental, WalkDirectoryBuilder, WalkDirectoryConfiguration, WalkDirectoryVisitorBuilder, WalkState, }; use crate::max_parallelism; @@ -173,7 +176,7 @@ impl System for OsSystem { /// Creates a builder to recursively walk `path`. /// - /// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`] + /// The walker ignores files according to [`::ignore::WalkBuilder::standard_filters`] /// when setting [`WalkDirectoryBuilder::standard_filters`] to true. fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { WalkDirectoryBuilder::new( @@ -268,7 +271,7 @@ impl DirectoryWalker for OsDirectoryWalker { return; }; - let mut builder = ignore::WalkBuilder::new(first.as_std_path()); + let mut builder = ::ignore::WalkBuilder::new(first.as_std_path()); builder.current_dir(self.cwd.as_std_path()); builder.standard_filters(standard_filters); @@ -315,7 +318,7 @@ impl DirectoryWalker for OsDirectoryWalker { })); // Skip the entire directory because all the paths won't be UTF-8 paths. - ignore::WalkState::Skip + ::ignore::WalkState::Skip } } } @@ -326,22 +329,40 @@ impl DirectoryWalker for OsDirectoryWalker { // (which, should not be reported here but the `ignore` crate doesn't distinguish between ignore and IO errors). // Let's log the error to at least make it visible. tracing::warn!("Failed to traverse directory: {error}."); - ignore::WalkState::Continue + ::ignore::WalkState::Continue } }, } }) }); } + + fn incremental_matcher( + &self, + configuration: WalkDirectoryConfiguration, + ) -> Box { + let WalkDirectoryConfiguration { + paths, + ignore_hidden: hidden, + standard_filters, + } = configuration; + + let mut builder = ::ignore::WalkBuilder::from_iter(paths.iter().map(|p| p.as_std_path())); + builder.current_dir(self.cwd.as_std_path()); + builder.standard_filters(standard_filters); + builder.hidden(hidden); + let root_matchers = builder.build_matchers(); + Box::new(IgnoreFiles { root_matchers }) + } } #[cold] fn ignore_to_walk_directory_error( - error: ignore::Error, + error: ::ignore::Error, path: Option, depth: Option, -) -> std::result::Result { - use ignore::Error; +) -> std::result::Result { + use ::ignore::Error; match error { Error::WithPath { path, err } => ignore_to_walk_directory_error(*err, Some(path), depth), @@ -399,12 +420,12 @@ impl From for FileType { } } -impl From for ignore::WalkState { +impl From for ::ignore::WalkState { fn from(value: WalkState) -> Self { match value { - WalkState::Continue => ignore::WalkState::Continue, - WalkState::Skip => ignore::WalkState::Skip, - WalkState::Quit => ignore::WalkState::Quit, + WalkState::Continue => ::ignore::WalkState::Continue, + WalkState::Skip => ::ignore::WalkState::Skip, + WalkState::Quit => ::ignore::WalkState::Quit, } } } diff --git a/crates/ruff_db/src/system/os/ignore.rs b/crates/ruff_db/src/system/os/ignore.rs new file mode 100644 index 0000000000..03c73e4e73 --- /dev/null +++ b/crates/ruff_db/src/system/os/ignore.rs @@ -0,0 +1,192 @@ +//! Checks whether paths are ignored during incremental project indexing. + +use crate::system::SystemPath; +use crate::system::walk_directory::IgnoreIncremental; + +pub(super) struct IgnoreFiles { + pub(super) root_matchers: Vec, +} + +impl IgnoreIncremental for IgnoreFiles { + fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool { + let Some((root, relative)) = self + .root_matchers + .iter_mut() + .filter_map(|root| { + let relative = path.as_std_path().strip_prefix(root.root()).ok()?; + Some((root, relative)) + }) + .max_by_key(|(root, _)| root.root().as_os_str().len()) + else { + return false; + }; + root.matched(relative, is_directory).is_ignore() + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use crate::system::{OsSystem, System, SystemPath, SystemPathBuf}; + + struct TestProject { + _temp_dir: TempDir, + system: OsSystem, + root: SystemPathBuf, + } + + impl TestProject { + fn new() -> Self { + Self::with_root("project") + } + + fn with_root(root: &str) -> Self { + let temp_dir = TempDir::new().unwrap(); + let temp_dir_path = SystemPath::from_std_path(temp_dir.path()).unwrap(); + let root = temp_dir_path.join(root); + std::fs::create_dir_all(root.as_std_path()).unwrap(); + let system = OsSystem::new(&root); + + Self { + _temp_dir: temp_dir, + system, + root, + } + } + + fn path(&self, relative_path: &str) -> SystemPathBuf { + self.root.join(relative_path) + } + + fn write_files<'a>(&self, files: impl IntoIterator) { + for (path, contents) in files { + std::fs::create_dir_all(path.parent().unwrap().as_std_path()).unwrap(); + std::fs::write(path.as_std_path(), contents).unwrap(); + } + } + + fn create_directory(&self, path: impl AsRef) { + std::fs::create_dir_all(path.as_ref().as_std_path()).unwrap(); + } + + fn is_ignored(&self, path: &SystemPath) -> bool { + self.is_ignored_from(std::slice::from_ref(&self.root), path) + } + + fn is_ignored_from(&self, walk_roots: &[SystemPathBuf], path: &SystemPath) -> bool { + let (first, additional) = walk_roots.split_first().unwrap(); + let mut builder = self.system.walk_directory(first); + + for root in additional { + builder = builder.add(root); + } + + builder.incremental_matcher().is_ignored(path, false) + } + } + + #[test] + fn root_ignore_file_prunes_top_level_directory() { + let project = TestProject::new(); + let path = project.path("build/keep.py"); + project.write_files([ + (project.path(".ignore"), "build/\n"), + (project.path("build/.ignore"), "!keep.py\n"), + ]); + + assert!(project.is_ignored(&path)); + } + + #[test] + fn root_gitignore_file_requires_repository() { + let project = TestProject::new(); + + let path = project.path("build/keep.py"); + project.write_files([(project.path(".gitignore"), "build/\n")]); + + assert!(!project.is_ignored(&path)); + } + + #[test] + fn bom() { + let project = TestProject::new(); + let path = project.path("build/keep.py"); + project.write_files([(project.path(".ignore"), "\u{feff}build/\n")]); + + assert!(project.is_ignored(&path)); + } + + #[test] + fn root_ignore_file_allowlist_overrides_root_gitignore_file() { + let project = TestProject::new(); + let path = project.path("build/keep.py"); + project.write_files([ + (project.path(".git/HEAD"), "ref: refs/heads/main\n"), + (project.path(".ignore"), "!build/\n"), + (project.path(".gitignore"), "build/\n"), + ]); + + assert!(!project.is_ignored(&path)); + } + + #[test] + fn parent_ignore_file_disables_root_gitignore_pruning() { + let project = TestProject::with_root("workspace/project"); + let path = project.path("build/keep.py"); + project.write_files([ + (project.path(".git/HEAD"), "ref: refs/heads/main\n"), + (project.path(".gitignore"), "build/\n"), + ( + project.root.parent().unwrap().join(".ignore"), + "!project/build/\n", + ), + ]); + + assert!(!project.is_ignored(&path)); + } + + #[test] + fn root_ignore_file_cannot_prune_deeper_file_match() { + let project = TestProject::new(); + let path = project.path("pkg/keep.py"); + project.write_files([(project.path(".ignore"), "pkg/keep.py\n")]); + + assert!(project.is_ignored(&path)); + } + + #[test] + fn unreadable_root_ignore_file_cannot_prune_path() { + let project = TestProject::new(); + let path = project.path("build/ignored.py"); + project.create_directory(project.path(".ignore")); + project.write_files([ + (project.path(".git/HEAD"), "ref: refs/heads/main\n"), + (project.path(".gitignore"), "build/\n"), + ]); + + assert!(project.is_ignored(&path)); + } + + #[test] + fn explicit_file_walk_root_cannot_be_pruned_by_parent_root() { + let project = TestProject::new(); + let path = project.path("build/keep.py"); + project.write_files([(project.path(".ignore"), "build/\n")]); + + assert!(!project.is_ignored_from(&[project.root.clone(), path.clone()], &path)); + } + + #[test] + fn nested_directory_walk_root_uses_its_own_ignore_file() { + let project = TestProject::new(); + let path = project.path("pkg/build/ignored.py"); + let nested_root = project.path("pkg"); + project.write_files([ + (project.path(".ignore"), "pkg/\n"), + (project.path("pkg/.ignore"), "build/\n"), + ]); + + assert!(project.is_ignored_from(&[project.root.clone(), nested_root], &path)); + } +} diff --git a/crates/ruff_db/src/system/walk_directory.rs b/crates/ruff_db/src/system/walk_directory.rs index 5796321c72..baa59f1c35 100644 --- a/crates/ruff_db/src/system/walk_directory.rs +++ b/crates/ruff_db/src/system/walk_directory.rs @@ -4,6 +4,12 @@ use std::path::PathBuf; use super::{FileType, SystemPath}; +/// A matcher for determining whether paths are ignored during incremental directory walking. +pub trait IgnoreIncremental { + /// Returns whether the directory walker ignores `path`. + fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool; +} + /// A builder for constructing a directory recursive traversal. pub struct WalkDirectoryBuilder { /// The implementation that does the directory walking. @@ -65,6 +71,16 @@ impl WalkDirectoryBuilder { self } + /// Creates a matcher for determining whether paths are ignored during incremental walking. + pub fn incremental_matcher(self) -> Box { + let configuration = WalkDirectoryConfiguration { + paths: self.paths, + ignore_hidden: self.ignore_hidden, + standard_filters: self.standard_filters, + }; + self.walker.incremental_matcher(configuration) + } + /// Runs the directory traversal and calls the passed `builder` to create visitors /// that do the visiting. The walker may run multiple threads to visit the directories. pub fn run<'s, F>(self, builder: F) @@ -94,6 +110,12 @@ pub trait DirectoryWalker { builder: &mut dyn WalkDirectoryVisitorBuilder, configuration: WalkDirectoryConfiguration, ); + + /// Creates a matcher for determining whether paths are ignored during incremental walking. + fn incremental_matcher( + &self, + configuration: WalkDirectoryConfiguration, + ) -> Box; } /// Creates a visitor for each thread that does the visiting. diff --git a/crates/ruff_db/src/testing.rs b/crates/ruff_db/src/testing.rs index c1096c45aa..e74d8a5bc0 100644 --- a/crates/ruff_db/src/testing.rs +++ b/crates/ruff_db/src/testing.rs @@ -227,10 +227,11 @@ fn query_was_not_run() { #[salsa::input(debug)] struct Input { + #[returns(clone)] text: String, } - #[salsa::tracked] + #[salsa::tracked(returns(copy))] fn len(db: &dyn salsa::Database, input: Input) -> usize { input.text(db).len() } @@ -262,10 +263,11 @@ fn query_was_not_run_fails_if_query_was_run() { #[salsa::input(debug)] struct Input { + #[returns(clone)] text: String, } - #[salsa::tracked] + #[salsa::tracked(returns(copy))] fn len(db: &dyn salsa::Database, input: Input) -> usize { input.text(db).len() } @@ -294,10 +296,11 @@ fn const_query_was_not_run_fails_if_query_was_run() { #[salsa::input] struct Input { + #[returns(clone)] text: String, } - #[salsa::tracked] + #[salsa::tracked(returns(copy))] fn len(db: &dyn salsa::Database) -> usize { db.report_untracked_read(); 5 @@ -325,10 +328,11 @@ fn query_was_run_fails_if_query_was_not_run() { #[salsa::input(debug)] struct Input { + #[returns(clone)] text: String, } - #[salsa::tracked] + #[salsa::tracked(returns(copy))] fn len(db: &dyn salsa::Database, input: Input) -> usize { input.text(db).len() } diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 264fdf4e2d..dc171cb425 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -556,7 +556,10 @@ fn format_dir_entry( ) -> anyhow::Result<(Result, PathBuf), Error> { let resolved_file = resolved_file.context("Iterating the files in the repository failed")?; // For some reason it does not filter in the beginning - if resolved_file.file_name() == "pyproject.toml" { + if ["pyproject.toml", "ruff.toml", ".ruff.toml"] + .iter() + .any(|&path| resolved_file.file_name() == path) + { return Ok((Ok(Statistics::default()), resolved_file.into_path())); } diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index cc716d94eb..233189ed8e 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index 8b27f8294e..c606f2fa91 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_diagnostics). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 6dc3c46140..fd5568e375 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 7dd4045a38..4701c296b6 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_formatter). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 478008aaa9..c239f328e8 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index 9646468294..6100ed4cc0 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_graph). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index f9395adadf..b9817e75b5 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index 61b53e2524..d99bf84ffb 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_index). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/src/frozen.rs b/crates/ruff_index/src/frozen.rs index f7ccf87f34..1a88b472a6 100644 --- a/crates/ruff_index/src/frozen.rs +++ b/crates/ruff_index/src/frozen.rs @@ -74,15 +74,7 @@ impl FromIterator for FrozenIndexVec { #[expect(unsafe_code)] unsafe impl Send for FrozenIndexVec where T: Send {} +// SAFETY: `FrozenIndexVec` owns its elements; `I` is only a marker. #[expect(unsafe_code)] #[cfg(feature = "salsa")] -unsafe impl salsa::Update for FrozenIndexVec -where - T: salsa::Update, -{ - #[expect(unsafe_code)] - unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { - let old_box: &mut FrozenIndexVec = unsafe { &mut *old_pointer }; - unsafe { salsa::Update::maybe_update(&raw mut old_box.raw, new_value.raw) } - } -} +unsafe impl salsa::SalsaValue for FrozenIndexVec {} diff --git a/crates/ruff_index/src/vec.rs b/crates/ruff_index/src/vec.rs index 648f9cb132..44c9734ffb 100644 --- a/crates/ruff_index/src/vec.rs +++ b/crates/ruff_index/src/vec.rs @@ -182,15 +182,7 @@ impl From<[T; N]> for IndexVec { #[expect(unsafe_code)] unsafe impl Send for IndexVec where T: Send {} +// SAFETY: `IndexVec` owns its elements; `I` is only a marker. #[expect(unsafe_code)] #[cfg(feature = "salsa")] -unsafe impl salsa::Update for IndexVec -where - T: salsa::Update, -{ - #[expect(unsafe_code)] - unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { - let old_vec: &mut IndexVec = unsafe { &mut *old_pointer }; - unsafe { salsa::Update::maybe_update(&raw mut old_vec.raw, new_value.raw) } - } -} +unsafe impl salsa::SalsaValue for IndexVec {} diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index d62d2f11b9..c2decabbbc 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.20" +version = "0.15.22" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -49,10 +49,7 @@ libcst = { workspace = true } log = { workspace = true } memchr = { workspace = true } natord = { workspace = true } -path-absolutize = { workspace = true, features = [ - "once_cell_cache", - "use_unix_paths_on_wasm", -] } +path-absolutize = { workspace = true, features = ["fixed_workdir"] } pep440_rs = { workspace = true } pyproject-toml = { workspace = true } regex = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index 3c3bbb91f1..ba776d0d57 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.20) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_linter). +This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/resources/mdtest/flake8-pyi/string-or-bytes-too-long.md b/crates/ruff_linter/resources/mdtest/flake8-pyi/string-or-bytes-too-long.md new file mode 100644 index 0000000000..c01174839c --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-pyi/string-or-bytes-too-long.md @@ -0,0 +1,16 @@ +# `string-or-bytes-too-long` (`PYI053`) + +```toml +[lint] +select = ["PYI053"] +``` + +## Long name in `__all__` + +Strings in `__all__` correspond to exported names and should be exempt from the rule. + +```pyi +__all__ = [ + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeef", +] +``` diff --git a/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md b/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md new file mode 100644 index 0000000000..6b360389ce --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md @@ -0,0 +1,86 @@ +# `redefined-loop-name` (`PLW2901`) + +```toml +[lint] +select = ["PLW2901"] +``` + +## Augmented assignment + +Ignore in-place update of a mutable type. + +```py +for i in []: + i += [1] + +for i in []: + i = [1] # snapshot: redefined-loop-name + +for i in []: + i |= {"a": 1} + +for i in []: + i = {"b": 2} # snapshot: redefined-loop-name + +for i in []: + i |= {1} + +for i in []: + i &= {1} + +for i in []: + i ^= {1} + +for i in []: + i -= {1} + +for i in []: + i = {1} # snapshot: redefined-loop-name + +for i in []: + i += (1,) # snapshot: redefined-loop-name + +for i in []: + i += "a" # snapshot: redefined-loop-name +``` + +```snapshot +error[PLW2901]: `for` loop variable `i` overwritten by assignment target + --> src/mdtest_snippet.py:5:5 + | +5 | i = [1] # snapshot: redefined-loop-name + | ^ + | + + +error[PLW2901]: `for` loop variable `i` overwritten by assignment target + --> src/mdtest_snippet.py:11:5 + | +11 | i = {"b": 2} # snapshot: redefined-loop-name + | ^ + | + + +error[PLW2901]: `for` loop variable `i` overwritten by assignment target + --> src/mdtest_snippet.py:26:5 + | +26 | i = {1} # snapshot: redefined-loop-name + | ^ + | + + +error[PLW2901]: `for` loop variable `i` overwritten by assignment target + --> src/mdtest_snippet.py:29:5 + | +29 | i += (1,) # snapshot: redefined-loop-name + | ^ + | + + +error[PLW2901]: `for` loop variable `i` overwritten by assignment target + --> src/mdtest_snippet.py:32:5 + | +32 | i += "a" # snapshot: redefined-loop-name + | ^ + | +``` diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/non-pep695-type-alias.md b/crates/ruff_linter/resources/mdtest/pyupgrade/non-pep695-type-alias.md new file mode 100644 index 0000000000..f0ffa45124 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/non-pep695-type-alias.md @@ -0,0 +1,34 @@ +# `non-pep695-type-alias` (`UP040`) + +## `TypeVar` defaults before Python 3.13 + +`typing_extensions` backports the `default` argument to Python 3.12 and earlier, but the PEP-695 +syntax enforced by the rule is only available on 3.13 and later, so we have to avoid a diagnostic in +both of these cases. + +```toml +target-version = "py312" + +[lint] +preview = true +select = ["non-pep695-type-alias"] +``` + +### `TypeAlias` + +```py +from typing import TypeAlias +from typing_extensions import TypeVar + +T = TypeVar("T", default=int) +Alias: TypeAlias = list[T] +``` + +### `TypeAliasType` + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T", default=int) +Alias = TypeAliasType("Alias", list[T], type_params=(T,)) +``` diff --git a/crates/ruff_linter/resources/mdtest/refurb/subclass-builtin.md b/crates/ruff_linter/resources/mdtest/refurb/subclass-builtin.md new file mode 100644 index 0000000000..5fc8433d19 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/refurb/subclass-builtin.md @@ -0,0 +1,20 @@ +# `subclass-builtin` (`FURB189`) + +```toml +[lint] +preview = true +select = ["FURB189"] +``` + +## Stub files + +Subclassing a builtin in a stub must be allowed so the stub can faithfully represent the runtime +implementation. + +```pyi +class D(dict): ... +class L(list): ... +class S(str): ... +class SubscriptDict(dict[str, str]): ... +class SubscriptList(list[str]): ... +``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md b/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md new file mode 100644 index 0000000000..033ca832e9 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md @@ -0,0 +1,39 @@ +# `invalid-pyproject-toml` (`RUF200`) + +```toml +[lint] +select = ["RUF200"] +``` + +## Reports an invalid `pyproject.toml` + +`pyproject.toml`: + +```toml +[project] +name = 1 # snapshot: invalid-pyproject-toml +``` + +```snapshot +error[RUF200]: Failed to parse pyproject.toml: invalid type: integer `1`, expected a string + --> src/pyproject.toml:2:8 + | +2 | name = 1 # snapshot: invalid-pyproject-toml + | ^ + | +``` + +## Respects per-file ignores + +```toml +[lint] +select = ["RUF200"] +per-file-ignores = { "pyproject.toml" = ["RUF200"] } +``` + +`pyproject.toml`: + +```toml +[project] +name = 1 +``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md new file mode 100644 index 0000000000..c84e2a8568 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md @@ -0,0 +1,399 @@ +# `noqa-comments` (`RUF105`) + +```toml +[lint] +preview = true +select = ["noqa-comments", "F401", "F402", "F403"] +``` + +## File-level comments + +### Single code + +```py +# snapshot: noqa-comments +# ruff: noqa: F401 +import math +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa: F401 + | ^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead + | +1 | # snapshot: noqa-comments + - # ruff: noqa: F401 +2 + # ruff:file-ignore[F401] +3 | import math + | +``` + +### Multiple codes + +```py +# snapshot: noqa-comments +# ruff: noqa: F401, F402, F403 +import math +import os +from module import * +for os in []: + pass +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa: F401, F402, F403 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead + | +1 | # snapshot: noqa-comments + - # ruff: noqa: F401, F402, F403 +2 + # ruff:file-ignore[F401, F402, F403] +3 | import math + | +``` + +### Multiple codes followed by a reason + +```py +# snapshot: noqa-comments +# ruff: noqa: F401, F402, F403 for some reason +import math +import os +from module import * +for os in []: + pass +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa: F401, F402, F403 for some reason + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead + | +1 | # snapshot: noqa-comments + - # ruff: noqa: F401, F402, F403 for some reason +2 + # ruff:file-ignore[F401, F402, F403] for some reason +3 | import math + | +``` + +### Multiple codes followed by a nested (pragma) comment + +```py +# snapshot: noqa-comments +# ruff: noqa: F401, F402, F403 # fmt:skip +import math +import os +from module import * +for os in []: + pass +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa: F401, F402, F403 # fmt:skip + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead + | +1 | # snapshot: noqa-comments + - # ruff: noqa: F401, F402, F403 # fmt:skip +2 + # ruff:file-ignore[F401, F402, F403] # fmt:skip +3 | import math + | +``` + +### Unknown codes still receive a diagnostic + +In case the unknown code is a typo rather than an intentionally external code, we emit both +`invalid-rule-code` and `noqa-comments`: + +```toml +[lint] +preview = true +select = ["noqa-comments", "unused-noqa", "invalid-rule-code", "F401"] +``` + +```py +# error: [invalid-rule-code] +# snapshot: noqa-comments +import math # noqa: F401, UNK001 +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:3:14 + | +3 | import math # noqa: F401, UNK001 + | ^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:ignore` instead + | +2 | # snapshot: noqa-comments + - import math # noqa: F401, UNK001 +3 + import math # ruff:ignore[F401, UNK001] + | +``` + +### External codes + +```toml +[lint] +preview = true +select = ["noqa-comments", "unused-noqa", "invalid-rule-code", "F401"] +external = ["EXT"] +``` + +If all of the codes are marked `external`, no diagnostic is emitted: + +```py +# error: [unused-import] +import math # noqa: EXT001, EXT002 +``` + +However, if only some of the codes are `external`, a diagnostic is emitted without an autofix. In +this case, the external codes likely need to remain in a `noqa` comment, while the codes known by +Ruff could potentially move into a `ruff:ignore` comment. + +```py +# snapshot: noqa-comments +import math # noqa: F401, EXT001 +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:4:14 + | +4 | import math # noqa: F401, EXT001 + | ^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:ignore` instead +``` + +### Any unmatched code disables the fix + +This leaves an unused `noqa` comment to be cleaned up by `RUF100` instead, which can be especially +important in the case of a standalone `noqa` comment, which has no effect (in almost all cases), but +could become an effectful own-line `ruff:ignore` comment if `RUF105` applied. + +```py +# snapshot: noqa-comments +# ruff: noqa: F401, F402 +import math +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa: F401, F402 + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead +``` + +### Flake8 comments are ignored + +```py +# flake8: noqa: F401 +import math +``` + +## Inline comments + +### Basic + +```py +# snapshot: noqa-comments +import math # noqa: F401 +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:2:14 + | +2 | import math # noqa: F401 + | ^^^^^^^^^^^^ + | +help: Use `ruff:ignore` instead + | +1 | # snapshot: noqa-comments + - import math # noqa: F401 +2 + import math # ruff:ignore[F401] + | +``` + +### One unmatched code + +Just like the file-level version above, this disables the autofix but not the rule. + +```py +# snapshot: noqa-comments +import os # noqa: F401, F402 +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:2:12 + | +2 | import os # noqa: F401, F402 + | ^^^^^^^^^^^^^^^^^^ + | +help: Use `ruff:ignore` instead +``` + +### Nested pragma comment before the directive + +```py +# snapshot: noqa-comments +import math # fmt:skip # noqa: F401 +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:2:25 + | +2 | import math # fmt:skip # noqa: F401 + | ^^^^^^^^^^^^ + | +help: Use `ruff:ignore` instead + | +1 | # snapshot: noqa-comments + - import math # fmt:skip # noqa: F401 +2 + import math # fmt:skip # ruff:ignore[F401] + | +``` + +## Blanket comments + +### Inline + +For inline comments, `RUF105` flags blanket comments and offers a fix containing the codes that are +actually suppressed: + +```py +# snapshot: noqa-comments +import math # noqa +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:2:14 + | +2 | import math # noqa + | ^^^^^^ + | +help: Use `ruff:ignore` instead + | +1 | # snapshot: noqa-comments + - import math # noqa +2 + import math # ruff:ignore[F401] +3 | # snapshot: noqa-comments + | +``` + +Multiple diagnostics on the same line don't cause duplicate codes in the final comment: + +```py +# snapshot: noqa-comments +import foo, bar # noqa +``` + +```snapshot +error[RUF105]: `noqa` comment used instead of `ruff:ignore` + --> src/mdtest_snippet.py:4:18 + | +4 | import foo, bar # noqa + | ^^^^^^ + | +help: Use `ruff:ignore` instead + | +3 | # snapshot: noqa-comments + - import foo, bar # noqa +4 + import foo, bar # ruff:ignore[F401] + | +``` + +### File-level + +For file-level comments, only a diagnostic is emitted, without a fix: + +```py +# snapshot: noqa-comments +# ruff: noqa +import math +``` + +```snapshot +error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` + --> src/mdtest_snippet.py:2:1 + | +2 | # ruff: noqa + | ^^^^^^^^^^^^ + | +help: Use `ruff:file-ignore` instead +``` + +## Inline self-suppression + +```toml +[lint] +preview = true +select = ["noqa-comments", "unused-noqa", "F401"] +``` + +It should be possible to suppress `RUF105` with a `noqa` comment: + +```py +value = 1 # noqa: RUF105 +``` + +But a suppression for `RUF100` should not prevent the rule from firing: + +```py +# error: [noqa-comments] +import math # noqa: RUF100, F401 +``` + +## Suppression with `ruff:ignore` + +```toml +[lint] +preview = true +select = ["noqa-comments", "unused-noqa", "F401"] +``` + +### Inline suppression + +```py +import math # noqa: F401 # ruff:ignore[RUF105] +``` + +### Standalone suppression + +```py +# ruff:ignore[RUF105] +# ruff: noqa: F401 +import math +``` + +### File-level suppression + +```py +# ruff:file-ignore[RUF105] +# ruff: noqa: F401 +import math +``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md new file mode 100644 index 0000000000..cd9bada104 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -0,0 +1,213 @@ +# `rule-codes-in-selectors` (`RUF201`) + +```toml +[lint] +preview = true +select = ["rule-codes-in-selectors"] +``` + +## Various quotes + +`ruff.toml`: + +```toml +[lint] +select = [ + "F401", # snapshot: rule-codes-in-selectors + 'F402', # snapshot: rule-codes-in-selectors + """F403""", # snapshot: rule-codes-in-selectors + '''F404''', # snapshot: rule-codes-in-selectors +] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:3:6 + | +3 | "F401", # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with `unused-import` + | +2 | select = [ + - "F401", # snapshot: rule-codes-in-selectors +3 + "unused-import", # snapshot: rule-codes-in-selectors +4 | 'F402', # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:4:6 + | +4 | 'F402', # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with `import-shadowed-by-loop-var` + | +3 | "F401", # snapshot: rule-codes-in-selectors + - 'F402', # snapshot: rule-codes-in-selectors +4 + 'import-shadowed-by-loop-var', # snapshot: rule-codes-in-selectors +5 | """F403""", # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:5:8 + | +5 | """F403""", # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with `undefined-local-with-import-star` + | +4 | 'F402', # snapshot: rule-codes-in-selectors + - """F403""", # snapshot: rule-codes-in-selectors +5 + """undefined-local-with-import-star""", # snapshot: rule-codes-in-selectors +6 | '''F404''', # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:6:8 + | +6 | '''F404''', # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with `late-future-import` + | +5 | """F403""", # snapshot: rule-codes-in-selectors + - '''F404''', # snapshot: rule-codes-in-selectors +6 + '''late-future-import''', # snapshot: rule-codes-in-selectors +7 | ] + | +``` + +## Invalid rule codes + +Invalid rule codes are not flagged, including nested quoting issues like `"'F401'"`, but valid codes +in the same selector are still analyzed: + +`ruff.toml`: + +```toml +[lint] +# snapshot: rule-codes-in-selectors +select = ["'F401'", "F402"] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:3:22 + | +3 | select = ["'F401'", "F402"] + | ^^^^ + | +help: Replace rule code with `import-shadowed-by-loop-var` + | +2 | # snapshot: rule-codes-in-selectors + - select = ["'F401'", "F402"] +3 + select = ["'F401'", "import-shadowed-by-loop-var"] + | +``` + +## Invalid selector shapes + +Just in case these ever make it past our actual config deserialization, the rule skips over +malformed selectors (e.g. table for `select`, non-table for `per-file-ignores`): + +`ruff.toml`: + +```toml +[lint] +select = { nested = ["F401"] } +per-file-ignores = ["F401"] +``` + +## Prefixes and names + +Prefixes and rule names are also left alone: + +`ruff.toml`: + +```toml +[lint] +select = ["F", "unused-import"] +``` + +## All selectors + +Test that we flag all selectors both in the `lint` table and in the deprecated top-level settings: + +`ruff.toml`: + +```toml +select = ["F401"] # error: [rule-codes-in-selectors] +extend-select = ["F841"] # error: [rule-codes-in-selectors] +fixable = ["E501"] # error: [rule-codes-in-selectors] +extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] +ignore = ["F401"] # error: [rule-codes-in-selectors] +extend-ignore = ["F841"] # error: [rule-codes-in-selectors] +per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] +extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] +unfixable = ["F401"] # error: [rule-codes-in-selectors] +extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] +extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] +extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors] + +[lint] +select = ["F401"] # error: [rule-codes-in-selectors] +extend-select = ["F841"] # error: [rule-codes-in-selectors] +fixable = ["E501"] # error: [rule-codes-in-selectors] +extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] +ignore = ["F401"] # error: [rule-codes-in-selectors] +extend-ignore = ["F841"] # error: [rule-codes-in-selectors] +per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] +extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] +unfixable = ["F401"] # error: [rule-codes-in-selectors] +extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] +extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] +extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors] +``` + +## `pyproject.toml` + +`pyproject.toml`: + +```toml +[tool.ruff] +ignore = ["F401"] # error: [rule-codes-in-selectors] + +[tool.ruff.lint] +select = ["F402"] # error: [rule-codes-in-selectors] +``` + +## `unfixable` + +Test that `rule-codes-in-selectors` and other TOML-specific lints respect the user's `unfixable` +settings: + +```toml +[lint] +preview = true +select = ["rule-codes-in-selectors"] +unfixable = ["rule-codes-in-selectors"] +``` + +`ruff.toml`: + +```toml +# snapshot: rule-codes-in-selectors +lint.select = ["F401"] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:2:17 + | +2 | lint.select = ["F401"] + | ^^^^ + | +help: Replace rule code with `unused-import` +``` + +This should also cover settings like `extend-unsafe-fixes` and `per-file-ignores`, all of which are +handled through the `LintContext`. diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md new file mode 100644 index 0000000000..e202300431 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md @@ -0,0 +1,308 @@ +# `rule-codes-in-suppression-comments` (`RUF106`) + +```toml +[lint] +preview = true +select = ["RUF106"] +external = ["EXT"] +``` + +## `ruff:ignore` + +Each Ruff rule code receives a separate diagnostic. Rule names and external or unknown codes are +preserved: + +```py +# snapshot: rule-codes-in-suppression-comments +# snapshot: rule-codes-in-suppression-comments +# ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] +value = 1 +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:15 + | +3 | # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] +3 + # ruff:ignore[unused-import, undefined-name, EXT001, UNKNOWN, F841] +4 | value = 1 + | + + +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:54 + | +3 | # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] +3 + # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, unused-variable] +4 | value = 1 + | +``` + +Valid human-readable names are unaffected: + +```py +# snapshot: rule-codes-in-suppression-comments +# snapshot: rule-codes-in-suppression-comments +# ruff:ignore[F401, undefined-name, F841] +value = 1 +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:7:15 + | +7 | # ruff:ignore[F401, undefined-name, F841] + | ^^^^ + | +help: Replace rule code with name + | +6 | # snapshot: rule-codes-in-suppression-comments + - # ruff:ignore[F401, undefined-name, F841] +7 + # ruff:ignore[unused-import, undefined-name, F841] +8 | value = 1 + | + + +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:7:37 + | +7 | # ruff:ignore[F401, undefined-name, F841] + | ^^^^ + | +help: Replace rule code with name + | +6 | # snapshot: rule-codes-in-suppression-comments + - # ruff:ignore[F401, undefined-name, F841] +7 + # ruff:ignore[F401, undefined-name, unused-variable] +8 | value = 1 + | +``` + +## `ruff:file-ignore` + +```py +# snapshot: rule-codes-in-suppression-comments +# snapshot: rule-codes-in-suppression-comments +# ruff:file-ignore[F401, F841] +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:20 + | +3 | # ruff:file-ignore[F401, F841] + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:file-ignore[F401, F841] +3 + # ruff:file-ignore[unused-import, F841] + | + + +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:26 + | +3 | # ruff:file-ignore[F401, F841] + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:file-ignore[F401, F841] +3 + # ruff:file-ignore[F401, unused-variable] + | +``` + +## Matched `ruff:disable` and `ruff:enable` + +Matching comments are reported and fixed together: + +```py +# snapshot: rule-codes-in-suppression-comments +# snapshot: rule-codes-in-suppression-comments +# ruff:disable[F401, undefined-name, F841] +value = 1 +# ruff:enable[F401, undefined-name, F841] +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:16 + | +3 | # ruff:disable[F401, undefined-name, F841] + | ^^^^ +4 | value = 1 +5 | # ruff:enable[F401, undefined-name, F841] + | ---- + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:disable[F401, undefined-name, F841] +3 + # ruff:disable[unused-import, undefined-name, F841] +4 | value = 1 + - # ruff:enable[F401, undefined-name, F841] +5 + # ruff:enable[unused-import, undefined-name, F841] + | + + +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:38 + | +3 | # ruff:disable[F401, undefined-name, F841] + | ^^^^ +4 | value = 1 +5 | # ruff:enable[F401, undefined-name, F841] + | ---- + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - # ruff:disable[F401, undefined-name, F841] +3 + # ruff:disable[F401, undefined-name, unused-variable] +4 | value = 1 + - # ruff:enable[F401, undefined-name, F841] +5 + # ruff:enable[F401, undefined-name, unused-variable] + | +``` + +## Unmatched `ruff:disable` + +An unmatched disable comment is still an effective suppression through the end of its indentation +level: + +```py +# snapshot: rule-codes-in-suppression-comments +# ruff:disable[F401] +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:2:16 + | +2 | # ruff:disable[F401] + | ^^^^ + | +help: Replace rule code with name + | +1 | # snapshot: rule-codes-in-suppression-comments + - # ruff:disable[F401] +2 + # ruff:disable[unused-import] + | +``` + +## Unmatched `ruff:enable` + +An unmatched enable comment is invalid and is left to `invalid-suppression-comment`: + +```py +# ruff:enable[F401] +``` + +## Redirected codes + +Redirected codes are replaced with the name of their canonical rule: + +```py +# snapshot: rule-codes-in-suppression-comments +# ruff:ignore[PGH001] +value = 1 +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:2:15 + | +2 | # ruff:ignore[PGH001] + | ^^^^^^ + | +help: Replace rule code with name + | +1 | # snapshot: rule-codes-in-suppression-comments + - # ruff:ignore[PGH001] +2 + # ruff:ignore[suspicious-eval-usage] +3 | value = 1 + | +``` + +## Nested suppression comments + +Only the rule codes within a nested suppression comment are replaced: + +```py +# snapshot: rule-codes-in-suppression-comments +# snapshot: rule-codes-in-suppression-comments +value = 1 # explanation # ruff:ignore[F401, F841] reason # another +``` + +```snapshot +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:40 + | +3 | value = 1 # explanation # ruff:ignore[F401, F841] reason # another + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - value = 1 # explanation # ruff:ignore[F401, F841] reason # another +3 + value = 1 # explanation # ruff:ignore[unused-import, F841] reason # another + | + + +error[RUF106]: Rule code used instead of name in suppression comment + --> src/mdtest_snippet.py:3:46 + | +3 | value = 1 # explanation # ruff:ignore[F401, F841] reason # another + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-suppression-comments + - value = 1 # explanation # ruff:ignore[F401, F841] reason # another +3 + value = 1 # explanation # ruff:ignore[F401, unused-variable] reason # another + | +``` + +## Comments without Ruff rule codes + +Comments containing only names and external or unknown codes are unchanged: + +```py +# ruff:ignore[unused-import, EXT001, UNKNOWN] +value = 1 +``` + +## Self-suppression + +The rule can be suppressed by its code or name: + +```py +# ruff:ignore[F401, RUF106] +value = 1 +``` + +```py +# ruff:ignore[F401, rule-codes-in-suppression-comments] +value = 1 +``` + +The diagnostic can also be suppressed with a `noqa` comment: + +```py +value = 1 # ruff:ignore[F401] # noqa: RUF106 +``` diff --git a/crates/ruff_linter/resources/mdtest/suppression/ignore.md b/crates/ruff_linter/resources/mdtest/suppression/ignore.md index 5c7b5f0d01..752f1dd757 100644 --- a/crates/ruff_linter/resources/mdtest/suppression/ignore.md +++ b/crates/ruff_linter/resources/mdtest/suppression/ignore.md @@ -226,7 +226,7 @@ values = [ ```toml [lint] preview = true -select = ["E501", "F401", "RUF10"] +select = ["E501", "F401", "RUF100", "RUF103", "RUF104"] ``` An intervening `ruff:ignore` directive shouldn't cause a `disable`/`enable` pair to be reported as @@ -304,7 +304,7 @@ def f(): ```toml [lint] preview = true -select = ["F401", "RUF10"] +select = ["F401", "RUF100", "RUF104"] ``` A `file-ignore` within a range suppression takes precedence and marks the `disable` as unused: @@ -568,7 +568,7 @@ help: Remove unused suppression ```toml [lint] preview = true -select = ["F401", "RUF10"] +select = ["F401", "RUF103", "RUF104"] ``` `ruff:ignore` comments nested within other comments should still work: @@ -618,7 +618,7 @@ import foo ```toml [lint] preview = true -select = ["F401", "RUF10"] +select = ["F401", "RUF103", "RUF104"] ``` Nested `disable` and `file-ignore` comments are also invalid and don't suppress diagnostics on the @@ -668,7 +668,7 @@ import foo ```toml [lint] preview = true -select = ["F401", "RUF10", "FIX002"] +select = ["F401", "RUF100", "FIX002"] ``` Nested suppression comments on a comment-only line are treated as trailing on the comment itself and @@ -700,7 +700,7 @@ a = 10 ```toml [lint] preview = true -select = ["E501", "F821", "RUF10"] +select = ["E501", "F821", "RUF100", "RUF103"] ``` `RUF100` should have an unsafe fix when deleting a leading suppression would change the placement @@ -774,7 +774,7 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] preview = true -select = ["E501", "RUF10", "FIX002"] +select = ["E501", "RUF100", "FIX002"] ``` Deleting either half of a `disable`/`enable` pair should make the fix unsafe if in a nested context: @@ -812,7 +812,7 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] preview = true -select = ["E501", "F401", "F821", "RUF10"] +select = ["E501", "F401", "F821", "RUF100", "RUF103"] ``` Removing a code from a multi-code suppression doesn't promote the later suppression, so the fix is @@ -846,7 +846,7 @@ help: Remove unused suppression ```toml [lint] preview = true -select = ["F821", "RUF10"] +select = ["F821", "RUF102", "RUF103"] ``` The `RUF102` fix should also be unsafe when it would promote a later suppression: @@ -882,7 +882,7 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] preview = true -select = ["F401", "F821", "RUF10"] +select = ["F401", "F821", "RUF100", "RUF103"] ``` The same applies to fixes for invalid suppression placement: diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_comments.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_comments.py new file mode 100644 index 0000000000..c49e0ce37e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_comments.py @@ -0,0 +1,25 @@ +import os + +print("something") + +import last + +import late # comment-late + +from late_paren1 import ( # comment-late_paren1 + value +) + +from late_paren2 import ( + value # comment-late_paren2 +) + +from late_paren3 import ( + value +) # comment-late_paren3 + +from late_paren4 import ( + value1, + value2, # comment-late_paren4 + value3, +) diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_docstring.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_docstring.py new file mode 100644 index 0000000000..779de756bb --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_docstring.py @@ -0,0 +1,5 @@ +"""module docstring""" + +print("something") + +import os diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_future.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_future.py new file mode 100644 index 0000000000..ef5e540d4b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_future.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +print("something") + +import os diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang.py new file mode 100644 index 0000000000..42c502e7be --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 + +print("something") + +import os diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang_docstring_and_future.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang_docstring_and_future.py new file mode 100644 index 0000000000..ec17d3c9c0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_shebang_docstring_and_future.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +"""docstring""" + +from __future__ import annotations + +print("something") + +import os diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF016.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF016.py index 815456fc93..f115b43509 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF016.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF016.py @@ -128,3 +128,7 @@ def func(): # Should emit for invalid access using generator var = [1, 2, 3][(x for x in ())] + +# Should still emit for a later invalid bound, even if an earlier bound is unrecognized +x = "x" +var = [1, 2, 3][x:"y"] diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 8957b34f39..a7d87cba62 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2835,34 +2835,7 @@ impl<'a> Checker<'a> { _ => {} } - let scope = self.semantic.current_scope(); - - if scope.kind.is_module() - && match parent { - Stmt::Assign(ast::StmtAssign { targets, .. }) => { - if let Some(Expr::Name(ast::ExprName { id, .. })) = targets.first() { - id == "__all__" - } else { - false - } - } - Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => { - if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { - id == "__all__" - } else { - false - } - } - Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => { - if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { - id == "__all__" - } else { - false - } - } - _ => false, - } - { + if self.in_dunder_all_assignment(parent) { let (all_names, all_flags) = self.semantic.extract_dunder_all_names(parent); if all_flags.intersects(DunderAllFlags::INVALID_OBJECT) { @@ -3329,6 +3302,41 @@ impl<'a> Checker<'a> { self.semantic.restore(snapshot); } + + /// Report whether a module-level `__all__` assignment is being visited. + /// + /// This differs from [`SemanticModel::in_dunder_all_definition`], which is set only while + /// adding bindings for the entries in `__all__`. + pub(crate) fn in_dunder_all_assignment(&self, parent: &Stmt) -> bool { + if !self.semantic.current_scope().kind.is_module() { + return false; + } + + match parent { + Stmt::Assign(ast::StmtAssign { targets, .. }) => { + if let Some(Expr::Name(ast::ExprName { id, .. })) = targets.first() { + id == "__all__" + } else { + false + } + } + Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => { + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + id == "__all__" + } else { + false + } + } + Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => { + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + id == "__all__" + } else { + false + } + } + _ => false, + } + } } struct ParsedAnnotationsCache<'a> { @@ -3592,6 +3600,11 @@ impl<'a> LintContext<'a> { (self.diagnostics.into_inner(), self.source_file) } + #[inline] + pub(crate) fn into_diagnostics(self) -> Vec { + self.diagnostics.into_inner() + } + #[inline] pub(crate) fn as_mut_vec(&mut self) -> &mut Vec { self.diagnostics.get_mut() diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index 695fd294aa..a3f8649e8f 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -1,6 +1,7 @@ use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; +use ruff_python_trivia::tab_offset; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -26,7 +27,7 @@ pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize { let tab_size = indent_width.as_usize(); for c in line.bytes() { match c { - b'\t' => indent = (indent / tab_size) * tab_size + tab_size, + b'\t' => indent += tab_offset(indent, tab_size), b' ' => indent += 1, _ => break, } diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index fbd82a6ed8..8de18382a7 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -48,6 +48,10 @@ pub(crate) fn check_noqa( let exemption = FileExemption::from(&file_noqa_directives); + // Generate diagnostics for suppression comments before applying suppressions so that the + // diagnostics can themselves be suppressed. + suppressions.check_rule_codes(context, locator); + // Indices of diagnostics that were ignored by a `noqa` directive. let mut ignored_diagnostics = vec![]; @@ -117,15 +121,14 @@ pub(crate) fn check_noqa( } } - // Diagnostics for unused/invalid range suppressions - suppressions.check_suppressions(context, locator); - - // Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself - // suppressed. - if context.is_rule_enabled(Rule::UnusedNOQA) + // Only migrate directives that don't require RUF100 cleanup first. + let check_unused_noqa = context.is_rule_enabled(Rule::UnusedNOQA) && analyze_directives - && !exemption.includes(Rule::UnusedNOQA) - { + && !exemption.includes(Rule::UnusedNOQA); + let check_noqa_comment = + context.is_rule_enabled(Rule::NoqaComments) && !exemption.enumerates(Rule::NoqaComments); + + if check_unused_noqa || check_noqa_comment { let directives = noqa_directives .lines() .iter() @@ -138,36 +141,59 @@ pub(crate) fn check_noqa( ); for (directive, matches, is_file_level) in directives { match directive { - Directive::All(directive) => { - if matches.is_empty() { - let edit = delete_comment(directive.range(), locator); + Directive::All(all) => { + if check_unused_noqa && matches.is_empty() { + let edit = delete_comment(all.range(), locator); let mut diagnostic = context.report_diagnostic( UnusedNOQA { codes: None, kind: ruff::rules::UnusedNOQAKind::Noqa, }, - directive.range(), + all.range(), ); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary); diagnostic.set_fix(Fix::safe_edit(edit)); + } else if check_noqa_comment { + ruff::rules::noqa_comments( + context, + locator, + is_file_level, + matches.is_empty(), + directive, + matches, + suppressions, + ); } } - Directive::Codes(directive) => { + Directive::Codes(codes) => { let mut disabled_codes = vec![]; let mut duplicated_codes = vec![]; - let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; let mut valid_codes = vec![]; let mut seen_codes = FxHashSet::default(); let mut self_ignore = false; - for original_code in directive.iter().map(Code::as_str) { + let mut suppress_noqa_comment = false; + for original_code in codes.iter().map(Code::as_str) { let code = get_redirect_target(original_code).unwrap_or(original_code); - if Rule::UnusedNOQA.noqa_code() == code { - self_ignore = true; - break; - } - if seen_codes.insert(original_code) { + if Rule::UnusedNOQA.noqa_code() == code { + self_ignore = true; + if context.is_rule_enabled(Rule::UnusedNOQA) { + valid_codes.push(original_code); + } else { + disabled_codes.push(original_code); + } + continue; + } + + if context.is_rule_enabled(Rule::NoqaComments) + && Rule::NoqaComments.noqa_code() == code + { + suppress_noqa_comment = true; + valid_codes.push(original_code); + continue; + } + let is_code_used = if is_file_level { context.iter().any(|diag| { diag.secondary_code().is_some_and(|noqa| *noqa == code) @@ -187,26 +213,21 @@ pub(crate) fn check_noqa( } else { disabled_codes.push(original_code); } - } else { - unknown_codes.push(original_code); } } else { duplicated_codes.push(original_code); } } - if self_ignore { - continue; - } - - if !(disabled_codes.is_empty() + let has_unused_codes = !(disabled_codes.is_empty() && duplicated_codes.is_empty() - && unmatched_codes.is_empty()) - { + && unmatched_codes.is_empty()); + + if check_unused_noqa && !self_ignore && has_unused_codes { let edit = if valid_codes.is_empty() { - delete_comment(directive.range(), locator) + delete_comment(codes.range(), locator) } else { - let original_text = locator.slice(directive.range()); + let original_text = locator.slice(codes.range()); let prefix = if is_file_level { if original_text.contains("flake8") { "# flake8: noqa: " @@ -218,7 +239,7 @@ pub(crate) fn check_noqa( }; Edit::range_replacement( format!("{}{}", prefix, valid_codes.join(", ")), - directive.range(), + codes.range(), ) }; let mut diagnostic = context.report_diagnostic( @@ -230,16 +251,29 @@ pub(crate) fn check_noqa( }), kind: ruff::rules::UnusedNOQAKind::Noqa, }, - directive.range(), + codes.range(), ); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary); diagnostic.set_fix(Fix::safe_edit(edit)); + } else if check_noqa_comment && !suppress_noqa_comment { + ruff::rules::noqa_comments( + context, + locator, + is_file_level, + has_unused_codes, + directive, + matches, + suppressions, + ); } } } } } + // Diagnostics for unused/invalid range suppressions + suppressions.check_suppressions(context, locator); + if context.is_rule_enabled(Rule::RedirectedNOQA) && !exemption.includes(Rule::RedirectedNOQA) { ruff::rules::redirected_noqa(context, &noqa_directives); ruff::rules::redirected_file_noqa(context, &file_noqa_directives); diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index 5cc8f7a908..dbfe48a308 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -115,7 +115,7 @@ mod tests { }; let diagnostics = LintContext::new(Path::new(""), line, &settings); check_physical_lines(&locator, &stylist, &indexer, &[], &settings, &diagnostics); - diagnostics.into_parts().0 + diagnostics.into_diagnostics() }; let line_length = LineLength::try_from(8).unwrap(); assert_eq!(check_with_max_line_length(line_length), vec![]); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 4a6490eaa3..645b9266a2 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1089,8 +1089,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "102") => rules::ruff::rules::InvalidRuleCode, (Ruff, "103") => rules::ruff::rules::InvalidSuppressionComment, (Ruff, "104") => rules::ruff::rules::UnmatchedSuppressionComment, + (Ruff, "105") => rules::ruff::rules::NoqaComments, + (Ruff, "106") => rules::ruff::rules::RuleCodesInSuppressionComments, (Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml, + (Ruff, "201") => rules::ruff::rules::RuleCodesInSelectors, + #[cfg(any(feature = "test-rules", test))] (Ruff, "900") => rules::ruff::rules::StableTestRule, #[cfg(any(feature = "test-rules", test))] diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index 5543d9a569..a16ad5dcfd 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -9,12 +9,10 @@ use crate::settings::types::CompiledPerFileIgnoreList; /// /// On WASM this just returns `.`. Otherwise, defer to [`path_absolutize::path_dedot::CWD`]. pub fn get_cwd() -> &'static Path { - #[cfg(target_arch = "wasm32")] - { - Path::new(".") + cfg_select! { + target_arch = "wasm32" => Path::new("."), + _ => path_absolutize::path_dedot::CWD.as_path(), } - #[cfg(not(target_arch = "wasm32"))] - path_absolutize::path_dedot::CWD.as_path() } /// Create a set with codes matching the pattern/code pairs. @@ -40,11 +38,9 @@ pub fn normalize_path>(path: P) -> PathBuf { /// Convert any path to an absolute path (based on the specified project root). pub fn normalize_path_to, R: AsRef>(path: P, project_root: R) -> PathBuf { - let path = path.as_ref(); - if let Ok(path) = path.absolutize_from(project_root.as_ref()) { - return path.to_path_buf(); - } - path.to_path_buf() + path.as_ref() + .absolutize_from(project_root.as_ref()) + .into_owned() } /// Convert an absolute path to be relative to the current working directory. diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index 87c064f8ab..3387a79a96 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -17,8 +17,9 @@ use ruff_python_parser::Parsed; use ruff_python_semantic::{ ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel, }; -use ruff_python_trivia::textwrap::indent; -use ruff_text_size::{Ranged, TextSize}; +use ruff_python_trivia::{PythonWhitespace, textwrap::indent}; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::cst::matchers::{match_aliases, match_import_from, match_statement}; use crate::fix; @@ -77,16 +78,23 @@ impl<'a> Importer<'a> { // Insert after the last top-level import. Insertion::end_of_statement(stmt, self.source, self.stylist).into_edit(&required_import) } else { - // Check if there are any future imports that we need to respect - if let Some(last_future_import) = self.find_last_future_import() { - // Insert after the last future import - Insertion::end_of_statement(last_future_import, self.source, self.stylist) - .into_edit(&required_import) - } else { - // Insert at the start of the file. - Insertion::start_of_file(self.python_ast, self.source, self.stylist, None) - .into_edit(&required_import) - } + self.add_at_start(&required_import) + } + } + + /// Add an existing import statement to the start of the file. + pub(crate) fn add_import_at_start(&self, import: &Stmt) -> Edit { + let range = TextRange::new(import.start(), self.source.line_end(import.end())); + self.add_at_start(self.source[range].trim_whitespace()) + } + + fn add_at_start(&self, text: &str) -> Edit { + if let Some(last_future_import) = self.find_last_future_import() { + Insertion::end_of_statement(last_future_import, self.source, self.stylist) + .into_edit(text) + } else { + Insertion::start_of_file(self.python_ast, self.source, self.stylist, None) + .into_edit(text) } } diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index 4a59b41a92..f971c43b4d 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -38,7 +38,6 @@ mod noqa; pub mod package; pub mod packaging; pub mod preview; -pub mod pyproject_toml; pub mod registry; mod renamer; mod rule_redirects; @@ -48,6 +47,7 @@ pub mod settings; pub mod source_kind; pub mod suppression; mod text_helpers; +pub mod toml; pub mod upstream_categories; mod violation; diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index cf621c85a5..0c6eb95f70 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -9,6 +9,7 @@ use unicode_width::UnicodeWidthChar; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_macros::CacheKey; +use ruff_python_trivia::tab_offset; use ruff_text_size::TextSize; /// The length of a line of text that is considered too long. @@ -201,7 +202,7 @@ impl LineWidthBuilder { for c in chars { match c { '\t' => { - let tab_offset = tab_size - (self.column % tab_size); + let tab_offset = tab_offset(self.column, tab_size); self.width += tab_offset; self.column += tab_offset; } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 4b9452eb8f..dd324a6bf5 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -369,7 +369,7 @@ pub fn check_path( ) } -const MAX_ITERATIONS: usize = 100; +pub(crate) const MAX_ITERATIONS: usize = 100; /// Add any missing suppression comments to the source code at the given `Path`. pub fn add_suppressions_to_path( @@ -676,7 +676,11 @@ where } #[expect(clippy::print_stderr)] -fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) { +pub(crate) fn report_failed_to_converge_error( + path: &Path, + transformed: &str, + diagnostics: &[Diagnostic], +) { let codes = collect_rule_codes(diagnostics.iter().filter_map(Diagnostic::secondary_code)); if cfg!(debug_assertions) { eprintln!( diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index 40653e672e..87afaae8bf 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -68,8 +68,6 @@ impl<'a> Locator<'a> { /// Finds the closest [`TextSize`] not exceeding the offset for which `is_char_boundary` is /// `true`. /// - /// Can be replaced with `str::floor_char_boundary` once it's stable. - /// /// ## Examples /// /// ``` @@ -106,16 +104,7 @@ impl<'a> Locator<'a> { /// ); /// ``` pub fn floor_char_boundary(&self, offset: TextSize) -> TextSize { - if offset >= self.text_len() { - self.text_len() - } else { - // We know that the character boundary is within four bytes. - (0u32..=3u32) - .map(TextSize::from) - .filter_map(|index| offset.checked_sub(index)) - .find(|offset| self.contents.is_char_boundary(offset.to_usize())) - .unwrap_or_default() - } + TextSize::try_from(self.contents.floor_char_boundary(offset.to_usize())).unwrap() } /// Take the source code between the given [`TextRange`]. diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index 02ee569471..1d6a6edf86 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -380,12 +380,12 @@ impl<'a> SarifResult<'a> { #[allow(clippy::unnecessary_wraps)] fn uri(diagnostic: &Diagnostic) -> Result { let path = normalize_path(&*diagnostic.expect_ruff_filename()); - #[cfg(not(target_arch = "wasm32"))] - return url::Url::from_file_path(&path) - .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display())) - .map(|u| u.to_string()); - #[cfg(target_arch = "wasm32")] - return Ok(format!("file://{}", path.display())); + cfg_select! { + target_arch = "wasm32" => Ok(format!("file://{}", path.display())), + _ => url::Url::from_file_path(&path) + .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display())) + .map(|url| url.to_string()), + } } fn from_message( diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index eb645456d8..f7fe29b14b 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -65,6 +65,15 @@ pub(crate) enum Directive<'a> { Codes(Codes<'a>), } +impl Ranged for Directive<'_> { + fn range(&self) -> TextRange { + match self { + Directive::All(all) => all.range(), + Directive::Codes(codes) => codes.range(), + } + } +} + #[derive(Debug)] pub(crate) struct All { range: TextRange, @@ -122,6 +131,10 @@ impl Codes<'_> { self.iter() .any(|code| *needle == get_redirect_target(code.as_str()).unwrap_or(code.as_str())) } + + pub(crate) fn len(&self) -> usize { + self.codes.len() + } } impl Ranged for Codes<'_> { @@ -1355,7 +1368,7 @@ mod tests { use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; - use crate::settings::{LinterSettings, flags, types::PreviewMode}; + use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; use crate::suppression::Suppressions; use crate::test::{print_messages, test_contents}; @@ -1417,18 +1430,8 @@ mod tests { fn add_suppressions_in( source: &str, suppression_kind: SuppressionKind, - preview: PreviewMode, + settings: &LinterSettings, ) -> Result { - let settings = LinterSettings { - preview, - ..LinterSettings::for_rules([ - Rule::MissingTypeFunctionArgument, - Rule::MissingReturnTypeUndocumentedPublicFunction, - Rule::UnsortedImports, - Rule::UnusedFunctionArgument, - Rule::UndocumentedPublicFunction, - ]) - }; let path = Path::new(""); let source_map = SourceMap::default(); let source_kind = SourceKind::from_source_code( @@ -1437,7 +1440,7 @@ mod tests { )? .ok_or_else(|| anyhow!("test file should be Python"))?; - let (count, fixed) = add_suppressions(path, &source_kind, &settings, suppression_kind); + let (count, fixed) = add_suppressions(path, &source_kind, settings, suppression_kind); let plural = if count == 1 { "" } else { "s" }; let mut output = String::new(); writeln!( @@ -1447,7 +1450,7 @@ mod tests { let source_kind = source_kind.updated(fixed, &source_map); let (second_count, fixed) = - add_suppressions(path, &source_kind, &settings, suppression_kind); + add_suppressions(path, &source_kind, settings, suppression_kind); if second_count > 0 { writeln!( output, @@ -1456,7 +1459,7 @@ mod tests { } let source_kind = source_kind.updated(fixed, &source_map); - let (diagnostics, _) = test_contents(&source_kind, path, &settings); + let (diagnostics, _) = test_contents(&source_kind, path, settings); if !diagnostics.is_empty() { writeln!( output, @@ -3054,7 +3057,12 @@ mod tests { pass "#, SuppressionKind::Noqa, - PreviewMode::Disabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + Rule::UnusedFunctionArgument, + Rule::UndocumentedPublicFunction, + ]), )?, @" Added 1 suppression @@ -3079,7 +3087,13 @@ mod tests { pass "#, SuppressionKind::Noqa, - PreviewMode::Enabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + Rule::UnusedFunctionArgument, + Rule::UndocumentedPublicFunction, + ]) + .with_preview_mode(), )?, @" Added 1 suppression @@ -3104,7 +3118,13 @@ mod tests { pass "#, SuppressionKind::Ignore, - PreviewMode::Enabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + Rule::UnusedFunctionArgument, + Rule::UndocumentedPublicFunction, + ]) + .with_preview_mode(), )?, @" Added 1 suppression @@ -3120,6 +3140,56 @@ mod tests { Ok(()) } + #[test] + fn add_noqa_ruf105() -> Result<()> { + let settings = + LinterSettings::for_rules([Rule::NoqaComments, Rule::UnusedImport]).with_preview_mode(); + + assert_snapshot!( + add_suppressions_in( + "import math # noqa: F401", + SuppressionKind::Noqa, + &settings, + )?, + @" + Added 1 suppression + + ## Fixed source + + ```py + import math # noqa: F401, RUF105 + + ``` + " + ); + Ok(()) + } + + #[test] + fn add_ignore_ruf105() -> Result<()> { + let settings = + LinterSettings::for_rules([Rule::NoqaComments, Rule::UnusedImport]).with_preview_mode(); + + assert_snapshot!( + add_suppressions_in( + "import math # noqa: F401", + SuppressionKind::Ignore, + &settings, + )?, + @" + Added 1 suppression + + ## Fixed source + + ```py + import math # noqa: F401 # ruff:ignore[noqa-comments] + + ``` + " + ); + Ok(()) + } + #[test] fn add_ignore_to_existing_ignore() -> Result<()> { assert_snapshot!( @@ -3129,7 +3199,13 @@ mod tests { pass "#, SuppressionKind::Ignore, - PreviewMode::Enabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + Rule::UnusedFunctionArgument, + Rule::UndocumentedPublicFunction, + ]) + .with_preview_mode(), )?, @" Added 1 suppression @@ -3154,7 +3230,12 @@ mod tests { pass "#, SuppressionKind::Ignore, - PreviewMode::Enabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + Rule::UndocumentedPublicFunction, + ]) + .with_preview_mode(), )?, @" Added 1 suppression @@ -3180,7 +3261,7 @@ mod tests { import a "#, SuppressionKind::Ignore, - PreviewMode::Enabled, + &LinterSettings::for_rules([Rule::UnsortedImports]).with_preview_mode(), )?, @" Added 1 suppression @@ -3208,7 +3289,11 @@ mod tests { return x "#, SuppressionKind::Ignore, - PreviewMode::Enabled, + &LinterSettings::for_rules([ + Rule::MissingTypeFunctionArgument, + Rule::MissingReturnTypeUndocumentedPublicFunction, + ]) + .with_preview_mode(), )?, @r#" Added 1 suppression diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 36db7f67f9..7de6ef4411 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -335,6 +335,11 @@ pub(crate) const fn is_incorrect_dict_iterator_comprehension_enabled( settings.preview.is_enabled() } +// https://github.com/astral-sh/ruff/pull/22212 +pub(crate) const fn is_e402_fix_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + // https://github.com/astral-sh/ruff/pull/23260 pub(crate) const fn is_up006_future_annotations_fix_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/pyproject_toml.rs deleted file mode 100644 index 8b319f595c..0000000000 --- a/crates/ruff_linter/src/pyproject_toml.rs +++ /dev/null @@ -1,61 +0,0 @@ -use colored::Colorize; -use log::warn; -use pyproject_toml::PyProjectToml; -use ruff_text_size::{TextRange, TextSize}; - -use ruff_db::diagnostic::Diagnostic; -use ruff_source_file::SourceFile; - -use crate::registry::Rule; -use crate::rules::ruff::rules::InvalidPyprojectToml; -use crate::settings::LinterSettings; -use crate::{IOError, Violation}; - -/// RUF200 -pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec { - let Some(err) = toml::from_str::(source_file.source_text()).err() else { - return Vec::default(); - }; - - let mut messages = Vec::new(); - let range = match err.span() { - // This is bad but sometimes toml and/or serde just don't give us spans - // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 - None => TextRange::default(), - Some(range) => { - let Ok(end) = TextSize::try_from(range.end) else { - let message = format!( - "{} is larger than 4GB, but ruff assumes all files to be smaller", - source_file.name(), - ); - if settings.rules.enabled(Rule::IOError) { - let diagnostic = - IOError { message }.into_diagnostic(TextRange::default(), source_file); - messages.push(diagnostic); - } else { - warn!( - "{}{}{} {message}", - "Failed to lint ".bold(), - source_file.name().bold(), - ":".bold() - ); - } - return messages; - }; - TextRange::new( - // start <= end, so if end < 4GB follows start < 4GB - TextSize::try_from(range.start).unwrap(), - end, - ) - } - }; - - if settings.rules.enabled(Rule::InvalidPyprojectToml) { - let toml_err = err.message().to_string(); - let diagnostic = - InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file); - messages.push(diagnostic); - } - - messages -} diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 766eee9852..21892be74d 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -241,7 +241,8 @@ pub enum LintSource { Imports, Noqa, Filesystem, - PyprojectToml, + /// A TOML config file, either `pyproject.toml`, `ruff.toml`, or `.ruff.toml`. + Toml, } impl Rule { @@ -249,8 +250,12 @@ impl Rule { /// physical lines). pub const fn lint_source(&self) -> LintSource { match self { - Rule::InvalidPyprojectToml => LintSource::PyprojectToml, - Rule::BlanketNOQA | Rule::RedirectedNOQA | Rule::UnusedNOQA => LintSource::Noqa, + Rule::InvalidPyprojectToml | Rule::RuleCodesInSelectors => LintSource::Toml, + Rule::BlanketNOQA + | Rule::NoqaComments + | Rule::RedirectedNOQA + | Rule::RuleCodesInSuppressionComments + | Rule::UnusedNOQA => LintSource::Noqa, Rule::BidirectionalUnicode | Rule::BlankLineWithWhitespace | Rule::DocLineTooLong diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index 7a477aeae9..70b6303c8d 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -15,6 +15,10 @@ use crate::rule_redirects::get_redirect; use crate::settings::types::PreviewMode; use crate::warn_user_once_by_message; +/// A potential rule selector that has not yet been validated and tracks its source. +/// +/// If you add a new field that uses this type, be sure to update `rule-codes-in-selectors` +/// (`RUF201`) to validate the additional selector field. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(transparent)] pub struct UnresolvedRuleSelector(RangedValue); diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 24be9c37df..64f1fac64d 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -399,19 +399,17 @@ impl<'a> Dependency<'a> { .map(|name| name.id.as_str()) }) .collect() - } else if let Some(method_def) = class_def - .body - .iter() - .filter_map(|stmt| stmt.as_function_def_stmt()) - .find(|func_def| func_def.name.as_str() == method_name) - { + } else { + let method_def = class_def + .body + .iter() + .filter_map(|stmt| stmt.as_function_def_stmt()) + .find(|func_def| func_def.name.as_str() == method_name)?; // Skip `self` parameter non_posonly_non_variadic_parameters(method_def) .skip(1) .map(|param| param.name().as_str()) .collect() - } else { - return None; }; Some(Self::Class(parameter_names)) diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs index 4545302ba0..061583826b 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -22,6 +22,10 @@ use crate::rules::flake8_datetimez::helpers; /// `datetime.datetime.today()` creates a "naive" object; instead, use /// `datetime.datetime.now(tz=...)` to create a timezone-aware object. /// +/// The name `today()` can be misleading, because it suggests a calendar date, +/// but it actually returns the current local date and time as a `datetime`. +/// That can make intent harder to infer when reading code. +/// /// ## Example /// ```python /// import datetime diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index b22a050ad0..ef7e47ceb2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -101,14 +101,14 @@ impl Violation for CustomTypeVarForSelf { fn message(&self) -> String { format!( "Use `Self` instead of custom TypeVar `{}`", - &self.typevar_name + self.typevar_name ) } fn fix_title(&self) -> Option { Some(format!( "Replace TypeVar `{}` with `Self`", - &self.typevar_name + self.typevar_name )) } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 0b0580663b..d4fd623c4d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -21,6 +21,9 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// checkers, the primary consumers of stub files. Replace very long constants /// with ellipses (`...`) to simplify the stub. /// +/// The rule does not apply to long entries in `__all__`, which are assumed to +/// be outside the stub author's control. +/// /// ## Example /// /// ```pyi @@ -51,8 +54,10 @@ impl AlwaysFixableViolation for StringOrBytesTooLong { pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) { let semantic = checker.semantic(); + let parent = semantic.current_statement(); + // Ignore docstrings. - if is_docstring_stmt(semantic.current_statement()) { + if is_docstring_stmt(parent) { return; } @@ -64,6 +69,10 @@ pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) { return; } + if checker.in_dunder_all_assignment(parent) { + return; + } + let length = match string { StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(), StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(), diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index 642ae0ab80..b2458326ee 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -16,8 +16,12 @@ use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## Why is this bad? /// By convention, environment variables should be capitalized. /// -/// On Windows, environment variables are case-insensitive and are converted to -/// uppercase, so using lowercase environment variables can lead to subtle bugs. +/// Furthermore, `os.environ` behaves differently across platforms. On Windows, +/// `os.environ` automatically converts environment variable names to uppercase. +/// This means that if you define a lowercase environment variable (e.g., `foo=1`), +/// iterating over `os.environ` will yield `FOO` on Windows, but +/// `foo` on Linux and macOS. This can lead to subtle bugs in cross-platform code +/// if it assumes environment variables preserve their original case. /// /// ## Example /// ```python diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 7ac29d7619..455646e0a1 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -76,6 +76,22 @@ mod tests { } #[test_case(Rule::LineTooLong, Path::new("E501_5.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_3.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_4.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_5.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_docstring.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_comments.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_future.py"))] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_shebang.py"))] + #[test_case( + Rule::ModuleImportNotAtTopOfFile, + Path::new("E402_shebang_docstring_and_future.py") + )] + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))] #[test_case(Rule::RedundantBackslash, Path::new("E502.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))] diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index 77f5ff6b35..47127a813e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -34,6 +34,15 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// return 2 * x /// ``` /// +/// ## Fix safety +/// This fix is marked as unsafe because converting a lambda assignment into a +/// function definition changes observable properties of the callable. +/// +/// In particular, a lambda function has the name `""`, while the +/// generated function uses the name of the assigned variable. Code that relies +/// on function metadata, such as logging, registration, or introspection, may +/// therefore behave differently after the fix. +/// /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.28")] diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs index 3da13717a3..0112808f32 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs @@ -1,9 +1,11 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{PySourceType, Stmt}; -use ruff_text_size::Ranged; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange}; -use crate::Violation; use crate::checkers::ast::Checker; +use crate::preview::is_e402_fix_enabled; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for imports that are not at the top of the file. @@ -38,6 +40,12 @@ use crate::checkers::ast::Checker; /// ## Notebook behavior /// For Jupyter notebooks, this rule checks for imports that are not at the top of a *cell*. /// +/// ## Fix safety +/// This rule's fix is marked as unsafe as imports moved to the top of the file +/// are placed above existing imports, in reverse order than they were in the +/// file. Re-ordering imports is unsafe as it can change the execution order of +/// the imported code. +/// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.28")] @@ -46,6 +54,8 @@ pub(crate) struct ModuleImportNotAtTopOfFile { } impl Violation for ModuleImportNotAtTopOfFile { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { if self.source_type.is_ipynb() { @@ -54,16 +64,54 @@ impl Violation for ModuleImportNotAtTopOfFile { "Module level import not at top of file".to_string() } } + + fn fix_title(&self) -> Option { + if self.source_type.is_ipynb() { + Some("Move module level imports to top of cell".to_string()) + } else { + Some("Move module level imports to top of file".to_string()) + } + } } /// E402 pub(crate) fn module_import_not_at_top_of_file(checker: &Checker, stmt: &Stmt) { if checker.semantic().seen_import_boundary() && checker.semantic().at_top_level() { - checker.report_diagnostic( + let mut diagnostic = checker.report_diagnostic( ModuleImportNotAtTopOfFile { source_type: checker.source_type, }, stmt.range(), ); + + if !is_e402_fix_enabled(checker.settings()) { + return; + } + + // Support for fixing notebooks is not yet implemented. + if checker.cell_offsets().is_some() { + return; + } + + let indexer = checker.indexer(); + let locator = checker.locator(); + + // Special-cases: there's leading or trailing content in the import block. These + // are too hard to get right, and relatively rare, so flag but don't fix. + if indexer.preceded_by_multi_statement_line(stmt, locator.contents()) + || indexer.followed_by_multi_statement_line(stmt, locator.contents()) + { + return; + } + + let edit = checker.importer().add_import_at_start(stmt); + + // Include trailing comments and the newline in the removal. + let removal_range = TextRange::new(stmt.start(), locator.full_line_end(stmt.end())); + + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_deletion(removal_range), + [edit], + )); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap index 137665e69c..e5e1c75287 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap @@ -11,6 +11,7 @@ E402 Module level import not at top of file 57 | #: E402 58 | import foo | +help: Move module level imports to top of file E402 Module level import not at top of file --> E40.py:58:1 @@ -22,6 +23,7 @@ E402 Module level import not at top of file 59 | 60 | a = 1 | +help: Move module level imports to top of file E402 Module level import not at top of file --> E40.py:62:1 @@ -33,6 +35,7 @@ E402 Module level import not at top of file 63 | 64 | #: E401 | +help: Move module level imports to top of file E402 Module level import not at top of file --> E40.py:65:1 @@ -42,6 +45,7 @@ E402 Module level import not at top of file | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 66 | import re as regex, string; x = 1 | +help: Move module level imports to top of file E402 Module level import not at top of file --> E40.py:66:1 @@ -53,6 +57,7 @@ E402 Module level import not at top of file 67 | 68 | x = 1; import re as regex, string | +help: Move module level imports to top of file E402 Module level import not at top of file --> E40.py:68:8 @@ -62,3 +67,4 @@ E402 Module level import not at top of file 68 | x = 1; import re as regex, string | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | +help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402.ipynb.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402.ipynb.snap index 470676927d..0895a0b946 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402.ipynb.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402.ipynb.snap @@ -11,6 +11,7 @@ E402 Module level import not at top of cell 10 | 11 | import a | +help: Move module level imports to top of cell E402 Module level import not at top of cell --> E402.ipynb:22:1 @@ -21,6 +22,7 @@ E402 Module level import not at top of cell | ^^^^^^^^ 23 | import ok | +help: Move module level imports to top of cell E402 Module level import not at top of cell --> E402.ipynb:30:1 @@ -30,3 +32,4 @@ E402 Module level import not at top of cell 31 | 32 | %%time | +help: Move module level imports to top of cell diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap index f3a5e4f3f1..758432087d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap @@ -9,6 +9,7 @@ E402 Module level import not at top of file 35 | import h | ^^^^^^^^ | +help: Move module level imports to top of file E402 Module level import not at top of file --> E402_0.py:45:1 @@ -18,6 +19,7 @@ E402 Module level import not at top of file 45 | import k; import l | ^^^^^^^^ | +help: Move module level imports to top of file E402 Module level import not at top of file --> E402_0.py:45:11 @@ -27,3 +29,4 @@ E402 Module level import not at top of file 45 | import k; import l | ^^^^^^^^ | +help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap index 2e45cb19b6..1309f16b55 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap @@ -11,6 +11,7 @@ E402 Module level import not at top of file 6 | 7 | """Some other docstring.""" | +help: Move module level imports to top of file E402 Module level import not at top of file --> E402_1.py:9:1 @@ -20,3 +21,4 @@ E402 Module level import not at top of file 9 | import c | ^^^^^^^^ | +help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap new file mode 100644 index 0000000000..178481cc69 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap @@ -0,0 +1,110 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E40.py:56:1 + | +54 | VERSION = '1.2.3' +55 | +56 | import foo + | ^^^^^^^^^^ +57 | #: E402 +58 | import foo + | +help: Move module level imports to top of file + | +1 | #: E401 +2 + import foo +3 | import os, sys +-------------------------------------------------------------------------------- +56 | + - import foo +57 | #: E402 + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E40.py:58:1 + | +56 | import foo +57 | #: E402 +58 | import foo + | ^^^^^^^^^^ +59 | +60 | a = 1 + | +help: Move module level imports to top of file + | +1 | #: E401 +2 + import foo +3 | import os, sys +-------------------------------------------------------------------------------- +58 | #: E402 + - import foo +59 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E40.py:62:1 + | +60 | a = 1 +61 | +62 | import bar + | ^^^^^^^^^^ +63 | +64 | #: E401 + | +help: Move module level imports to top of file + | +1 | #: E401 +2 + import bar +3 | import os, sys +-------------------------------------------------------------------------------- +62 | + - import bar +63 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E40.py:65:1 + | +64 | #: E401 +65 | import re as regex, string # also with a comment! + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +66 | import re as regex, string; x = 1 + | +help: Move module level imports to top of file + | +1 | #: E401 +2 + import re as regex, string # also with a comment! +3 | import os, sys +-------------------------------------------------------------------------------- +65 | #: E401 + - import re as regex, string # also with a comment! +66 | import re as regex, string; x = 1 + | +note: This is an unsafe fix and may change runtime behavior + +E402 Module level import not at top of file + --> E40.py:66:1 + | +64 | #: E401 +65 | import re as regex, string # also with a comment! +66 | import re as regex, string; x = 1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +67 | +68 | x = 1; import re as regex, string + | +help: Move module level imports to top of file + +E402 Module level import not at top of file + --> E40.py:68:8 + | +66 | import re as regex, string; x = 1 +67 | +68 | x = 1; import re as regex, string + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402.ipynb.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402.ipynb.snap new file mode 100644 index 0000000000..0895a0b946 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402.ipynb.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 Module level import not at top of cell + --> E402.ipynb:9:1 + | + 7 | os.path + 8 | + 9 | import pathlib + | ^^^^^^^^^^^^^^ +10 | +11 | import a + | +help: Move module level imports to top of cell + +E402 Module level import not at top of cell + --> E402.ipynb:22:1 + | +20 | __some__magic = 1 +21 | +22 | import c + | ^^^^^^^^ +23 | import ok + | +help: Move module level imports to top of cell + +E402 Module level import not at top of cell + --> E402.ipynb:30:1 + | +30 | import no_ok + | ^^^^^^^^^^^^ +31 | +32 | %%time + | +help: Move module level imports to top of cell diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap new file mode 100644 index 0000000000..7701ecae60 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_0.py:35:1 + | +33 | __some__magic = 1 +34 | +35 | import h + | ^^^^^^^^ + | +help: Move module level imports to top of file + | +1 | """Top-level docstring.""" +2 + import h +3 | +-------------------------------------------------------------------------------- +35 | + - import h +36 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 Module level import not at top of file + --> E402_0.py:45:1 + | +43 | import j +44 | +45 | import k; import l + | ^^^^^^^^ + | +help: Move module level imports to top of file + +E402 Module level import not at top of file + --> E402_0.py:45:11 + | +43 | import j +44 | +45 | import k; import l + | ^^^^^^^^ + | +help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap new file mode 100644 index 0000000000..45ed06923e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_1.py:5:1 + | +3 | """Some other docstring.""" +4 | +5 | import b + | ^^^^^^^^ +6 | +7 | """Some other docstring.""" + | +help: Move module level imports to top of file + | +1 + import b +2 | import a +3 | +4 | """Some other docstring.""" +5 | + - import b +6 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_1.py:9:1 + | +7 | """Some other docstring.""" +8 | +9 | import c + | ^^^^^^^^ + | +help: Move module level imports to top of file + | +1 + import c +2 | import a +-------------------------------------------------------------------------------- +9 | + - import c + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_2.py.snap new file mode 100644 index 0000000000..6dcc4546f1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_2.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap new file mode 100644 index 0000000000..6dcc4546f1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_4.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_4.py.snap new file mode 100644 index 0000000000..6dcc4546f1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_4.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_5.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_5.py.snap new file mode 100644 index 0000000000..6dcc4546f1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_5.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap new file mode 100644 index 0000000000..605ceda0f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap @@ -0,0 +1,156 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_comments.py:5:1 + | +3 | print("something") +4 | +5 | import last + | ^^^^^^^^^^^ +6 | +7 | import late # comment-late + | +help: Move module level imports to top of file + | +1 + import last +2 | import os +3 | +4 | print("something") +5 | + - import last +6 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_comments.py:7:1 + | +5 | import last +6 | +7 | import late # comment-late + | ^^^^^^^^^^^ +8 | +9 | from late_paren1 import ( # comment-late_paren1 + | +help: Move module level imports to top of file + | +1 + import late # comment-late +2 | import os +-------------------------------------------------------------------------------- +7 | + - import late # comment-late +8 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_comments.py:9:1 + | + 7 | import late # comment-late + 8 | + 9 | / from late_paren1 import ( # comment-late_paren1 +10 | | value +11 | | ) + | |_^ +12 | +13 | from late_paren2 import ( + | +help: Move module level imports to top of file + | +1 + from late_paren1 import ( # comment-late_paren1 +2 + value +3 + ) +4 | import os +-------------------------------------------------------------------------------- +11 | + - from late_paren1 import ( # comment-late_paren1 + - value + - ) +12 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_comments.py:13:1 + | +11 | ) +12 | +13 | / from late_paren2 import ( +14 | | value # comment-late_paren2 +15 | | ) + | |_^ +16 | +17 | from late_paren3 import ( + | +help: Move module level imports to top of file + | +1 + from late_paren2 import ( +2 + value # comment-late_paren2 +3 + ) +4 | import os +-------------------------------------------------------------------------------- +15 | + - from late_paren2 import ( + - value # comment-late_paren2 + - ) +16 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_comments.py:17:1 + | +15 | ) +16 | +17 | / from late_paren3 import ( +18 | | value +19 | | ) # comment-late_paren3 + | |_^ +20 | +21 | from late_paren4 import ( + | +help: Move module level imports to top of file + | +1 + from late_paren3 import ( +2 + value +3 + ) # comment-late_paren3 +4 | import os +-------------------------------------------------------------------------------- +19 | + - from late_paren3 import ( + - value + - ) # comment-late_paren3 +20 | + | +note: This is an unsafe fix and may change runtime behavior + +E402 [*] Module level import not at top of file + --> E402_comments.py:21:1 + | +19 | ) # comment-late_paren3 +20 | +21 | / from late_paren4 import ( +22 | | value1, +23 | | value2, # comment-late_paren4 +24 | | value3, +25 | | ) + | |_^ + | +help: Move module level imports to top of file + | +1 + from late_paren4 import ( +2 + value1, +3 + value2, # comment-late_paren4 +4 + value3, +5 + ) +6 | import os +-------------------------------------------------------------------------------- +25 | + - from late_paren4 import ( + - value1, + - value2, # comment-late_paren4 + - value3, + - ) + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap new file mode 100644 index 0000000000..1fc4af4423 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_docstring.py:5:1 + | +3 | print("something") +4 | +5 | import os + | ^^^^^^^^^ + | +help: Move module level imports to top of file + | +1 | """module docstring""" +2 + import os +3 | +4 | print("something") +5 | + - import os + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap new file mode 100644 index 0000000000..e7598f7f42 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_future.py:5:1 + | +3 | print("something") +4 | +5 | import os + | ^^^^^^^^^ + | +help: Move module level imports to top of file + | +1 | from __future__ import annotations +2 + import os +3 | +4 | print("something") +5 | + - import os + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap new file mode 100644 index 0000000000..0fa105895f --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_shebang.py:5:1 + | +3 | print("something") +4 | +5 | import os + | ^^^^^^^^^ + | +help: Move module level imports to top of file + | +2 | +3 + import os +4 | print("something") +5 | + - import os + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap new file mode 100644 index 0000000000..1f30b27fde --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E402 [*] Module level import not at top of file + --> E402_shebang_docstring_and_future.py:9:1 + | +7 | print("something") +8 | +9 | import os + | ^^^^^^^^^ + | +help: Move module level imports to top of file + | +5 | from __future__ import annotations +6 + import os +7 | +8 | print("something") +9 | + - import os + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs index 6b1c7f4cf1..19081b0c88 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs @@ -35,6 +35,11 @@ use crate::rules::pydocstyle::helpers::logical_line; /// """Return the mean of the given values.""" /// ``` /// +/// ## Fix safety +/// This fix is marked as unsafe, as it may alter the intended formatting of the +/// docstring, or affect tools that parse docstrings and rely on specific +/// formatting. +/// /// ## Options /// - `lint.pydocstyle.convention` /// diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs index 28a24babde..d853f603a5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs @@ -50,16 +50,8 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { }; match FormatSpec::parse(format_spec) { - Err(FormatSpecError::InvalidFormatType) => { - checker.report_diagnostic( - BadStringFormatCharacter { - // The format type character is always the last one. - // More info in the official spec: - // https://docs.python.org/3/library/string.html#format-specification-mini-language - format_char: format_spec.chars().last().unwrap(), - }, - range, - ); + Err(FormatSpecError::InvalidFormatType(format_char)) => { + checker.report_diagnostic(BadStringFormatCharacter { format_char }, range); } Err(_) => {} Ok(FormatSpec::Static(_)) => {} @@ -68,18 +60,11 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { let FormatPart::Field { format_spec, .. } = placeholder else { continue; }; - if let Err(FormatSpecError::InvalidFormatType) = + if let Err(FormatSpecError::InvalidFormatType(format_char)) = FormatSpec::parse(&format_spec) { - checker.report_diagnostic( - BadStringFormatCharacter { - // The format type character is always the last one. - // More info in the official spec: - // https://docs.python.org/3/library/string.html#format-specification-mini-language - format_char: format_spec.chars().last().unwrap(), - }, - range, - ); + checker + .report_diagnostic(BadStringFormatCharacter { format_char }, range); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs index 3241182541..82c4da9d05 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, helpers::is_dunder, name::QualifiedName}; -use ruff_python_semantic::{FromImport, Import, Imported, ResolvedReference, Scope}; +use ruff_python_semantic::{AnyImport, FromImport, Import, Imported, ResolvedReference, Scope}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::Ranged; @@ -79,9 +79,9 @@ pub(crate) fn import_private_name(checker: &Checker, scope: &Scope) { }; let import_info = match import { - import if import.is_import() => ImportInfo::from(import.import().unwrap()), - import if import.is_from_import() => ImportInfo::from(import.from_import().unwrap()), - _ => continue, + AnyImport::Import(import) => ImportInfo::from(import), + AnyImport::FromImport(import) => ImportInfo::from(import), + AnyImport::SubmoduleImport(_) => continue, }; let Some(root_module) = import_info.module_name.first() else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs index 7c743e3021..96d3fef241 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs @@ -7,6 +7,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing::is_mutable_expr; use ruff_text_size::Ranged; use crate::Violation; @@ -193,7 +194,23 @@ impl<'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'_, 'b> { ), ); } - Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => { + Stmt::AugAssign(ast::StmtAugAssign { + target, value, op, .. + }) => { + // Check for in-place update of mutable type + if is_mutable_expr(value, self.context) + && matches!( + op, + ast::Operator::Add + | ast::Operator::Sub + | ast::Operator::BitOr + | ast::Operator::BitAnd + | ast::Operator::BitXor + ) + { + return; + } + self.assignment_targets.extend( assignment_targets_from_expr(target, self.dummy_variable_rgx).map(|expr| { ExprWithInnerBindingKind { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_abc_decorator.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_abc_decorator.rs index 5cf42362a9..a9ad1786f6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_abc_decorator.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_abc_decorator.rs @@ -52,7 +52,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// def prop(self): ... /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(preview_since = "0.15.21")] pub(crate) struct DeprecatedAbcDecorator { from: &'static str, to: &'static str, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index dfa945fab1..06ad8060e9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -294,11 +294,11 @@ fn fix_always_false_branch( range, .. }) => { - debug_assert!( + debug_assert_eq!( checker .locator() - .slice(TextRange::at(range.start(), "elif".text_len())) - == "elif" + .slice(TextRange::at(range.start(), "elif".text_len())), + "elif" ); let end_location = range.start() + ("elif".text_len() - "if".text_len()); Some(Fix::unsafe_edit(Edit::deletion( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index f8f18c213e..f786b36c8c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -238,13 +238,6 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) { .unique_by(|tvar| tvar.name) .collect::>(); - // Skip if any TypeVar has defaults and preview mode is not enabled - if vars.iter().any(|tv| tv.default.is_some()) - && !is_type_var_default_enabled(checker.settings()) - { - return; - } - create_diagnostic( checker, stmt.into(), @@ -264,6 +257,15 @@ fn create_diagnostic( type_vars: &[TypeVar], type_alias_kind: TypeAliasKind, ) { + // If any type variables have defaults, skip the rule unless + // running with preview mode enabled and targeting Python 3.13+. + if (checker.target_version() < PythonVersion::PY313 + || !is_type_var_default_enabled(checker.settings())) + && type_vars.iter().any(|type_var| type_var.default.is_some()) + { + return; + } + let source = checker.source(); let tokens = checker.tokens(); let comment_ranges = checker.comment_ranges(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index a5605391bd..8c418ae40c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -133,7 +133,7 @@ fn replace_with_bytes_literal(locator: &Locator, call: &ast::ExprCall, tokens: & let _ = write!( &mut replacement, "b{}", - &string.trim_start_matches('u').trim_start_matches('U') + string.trim_start_matches('u').trim_start_matches('U') ); } _ => { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs index f5a7fb93a5..85777ae17d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeSet, HashMap}; -use itertools::{Itertools, chain}; +use itertools::Itertools; use ruff_python_semantic::NodeId; use ruff_macros::{ViolationMetadata, derive_message_formats}; @@ -45,16 +45,15 @@ use crate::{AlwaysFixableViolation, Applicability, Fix}; /// - [Python documentation: `__future__` — Future statement definitions](https://docs.python.org/3/library/__future__.html) #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.155")] -pub(crate) struct UnnecessaryFutureImport { - pub names: Vec, +pub(crate) struct UnnecessaryFutureImport<'a> { + pub names: &'a [&'a str], } -impl AlwaysFixableViolation for UnnecessaryFutureImport { +impl AlwaysFixableViolation for UnnecessaryFutureImport<'_> { #[derive_message_formats] fn message(&self) -> String { let UnnecessaryFutureImport { names } = self; - if names.len() == 1 { - let import = &names[0]; + if let [import] = names { format!("Unnecessary `__future__` import `{import}` for target Python version") } else { let imports = names.iter().map(|name| format!("`{name}`")).join(", "); @@ -67,26 +66,16 @@ impl AlwaysFixableViolation for UnnecessaryFutureImport { } } -const PY33_PLUS_REMOVE_FUTURES: &[&str] = &[ +const REMOVE_FUTURES: &[&str] = &[ + // Removed in Python 3.3 "nested_scopes", "generators", "with_statement", "division", "absolute_import", - "with_statement", - "print_function", - "unicode_literals", -]; - -const PY37_PLUS_REMOVE_FUTURES: &[&str] = &[ - "nested_scopes", - "generators", - "with_statement", - "division", - "absolute_import", - "with_statement", "print_function", "unicode_literals", + // Removed in Python 3.7 "generator_stop", ]; @@ -131,7 +120,7 @@ pub(crate) fn is_import_required_by_isort( /// UP010 pub(crate) fn unnecessary_future_import(checker: &Checker, scope: &Scope) { let mut unused_imports: HashMap> = HashMap::new(); - for future_name in chain(PY33_PLUS_REMOVE_FUTURES, PY37_PLUS_REMOVE_FUTURES).unique() { + for future_name in REMOVE_FUTURES { for binding_id in scope.get_all(future_name) { let binding = checker.semantic().binding(binding_id); if binding.kind.is_future_import() && binding.is_unused() { @@ -170,14 +159,13 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, scope: &Scope) { reason = "each import statement produces an independent diagnostic and fix" )] for (node_id, unused_aliases) in unused_imports { + let names: Vec<_> = unused_aliases + .iter() + .map(|alias| alias.name.as_str()) + .sorted() + .collect(); let mut diagnostic = checker.report_diagnostic( - UnnecessaryFutureImport { - names: unused_aliases - .iter() - .map(|alias| alias.name.to_string()) - .sorted() - .collect(), - }, + UnnecessaryFutureImport { names: &names }, checker.semantic().statement(node_id).range(), ); @@ -185,10 +173,7 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, scope: &Scope) { let statement = checker.semantic().statement(node_id); let parent = checker.semantic().parent_statement(node_id); let edit = fix::edits::remove_unused_imports( - unused_aliases - .iter() - .map(|alias| &alias.name) - .map(ast::Identifier::as_str), + names.into_iter(), statement, parent, checker.locator(), diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index a05ea4241b..e3acbbb7a5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -321,24 +321,6 @@ help: Use the `type` keyword 86 | | -UP040 [*] Type alias `AnyList` uses `TypeAliasType` assignment instead of the `type` keyword - --> UP040.py:95:1 - | -93 | # `default` was added in Python 3.13 -94 | T = typing.TypeVar("T", default=Any) -95 | AnyList = TypeAliasType("AnyList", list[T], type_params=(T,)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -96 | -97 | # unsafe fix if comments within the fix - | -help: Use the `type` keyword - | -94 | T = typing.TypeVar("T", default=Any) - - AnyList = TypeAliasType("AnyList", list[T], type_params=(T,)) -95 + type AnyList[T = Any] = list[T] -96 | - | - UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword --> UP040.py:99:1 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap index d26e2ae8e4..f2eed99224 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap @@ -7,7 +7,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- Summary --- Removed: 0 -Added: 2 +Added: 3 --- Added --- UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword @@ -30,6 +30,25 @@ help: Use the `type` keyword note: This is an unsafe fix and may change runtime behavior +UP040 [*] Type alias `AnyList` uses `TypeAliasType` assignment instead of the `type` keyword + --> UP040.py:95:1 + | +93 | # `default` was added in Python 3.13 +94 | T = typing.TypeVar("T", default=Any) +95 | AnyList = TypeAliasType("AnyList", list[T], type_params=(T,)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +96 | +97 | # unsafe fix if comments within the fix + | +help: Use the `type` keyword + | +94 | T = typing.TypeVar("T", default=Any) + - AnyList = TypeAliasType("AnyList", list[T], type_params=(T,)) +95 + type AnyList[T = Any] = list[T] +96 | + | + + UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `type` keyword --> UP040.py:134:1 | diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs index c6f5e1a3ed..90b8d10592 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs @@ -389,15 +389,16 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - node_index: _, value: string_val, }), - ) if operand.is_number_literal_expr() => operand.as_number_literal_expr().is_some_and( - |ast::ExprNumberLiteral { value, .. }| { - // Only support prefix removal for size at most `u32::MAX` - value - .as_int() - .and_then(ast::Int::as_usize) - .is_some_and(|x| x == string_val.chars().count()) - }, - ), + ) if let ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(value), + .. + }) = &**operand => + { + // Only support prefix removal for size at most `u32::MAX` + value + .as_usize() + .is_some_and(|x| x == string_val.chars().count()) + } ( AffixKind::EndsWith, ast::Expr::UnaryOp(ast::ExprUnaryOp { diff --git a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs index 20cb03109a..f44adf4374 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs @@ -15,6 +15,9 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// Use the `UserDict`, `UserList`, and `UserString` objects from the `collections` module /// instead. /// +/// This rule does not apply to stub files, which should faithfully represent the runtime +/// implementation and may be out of the author's control. +/// /// ## Example /// /// ```python @@ -83,6 +86,10 @@ impl AlwaysFixableViolation for SubclassBuiltin { /// FURB189 pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) { + if checker.source_type.is_stub() { + return; + } + let Some(Arguments { args: bases, .. }) = class.arguments.as_deref() else { return; }; diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 68bd7b1726..4252591fd1 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -7,23 +7,20 @@ pub(crate) mod typing; #[cfg(test)] mod tests { - use std::fs; use std::path::Path; use anyhow::Result; use regex::Regex; - use ruff_python_ast::PythonVersion; - use ruff_source_file::SourceFileBuilder; + use ruff_python_ast::{PythonVersion, TomlSourceType}; use rustc_hash::FxHashSet; use test_case::test_case; - use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; use crate::rules::pydocstyle::settings::Settings as PydocstyleSettings; use crate::settings::LinterSettings; use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode}; use crate::source_kind::SourceKind; - use crate::test::{test_contents, test_path, test_resource_path, test_snippet}; + use crate::test::{test_contents, test_path, test_resource_path, test_snippet, test_toml_path}; use crate::{UnresolvedRuleSelector, assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] @@ -789,17 +786,13 @@ mod tests { #[test_case(Rule::InvalidPyprojectToml, Path::new("pep639"))] fn invalid_pyproject_toml(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let path = test_resource_path("fixtures") - .join("ruff") - .join("pyproject_toml") - .join(path) - .join("pyproject.toml"); - let contents = fs::read_to_string(path)?; - let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish(); - let messages = lint_pyproject_toml( - &source_file, + let messages = test_toml_path( + Path::new("ruff/pyproject_toml") + .join(path) + .join("pyproject.toml"), &settings::LinterSettings::for_rule(Rule::InvalidPyprojectToml), - ); + TomlSourceType::Pyproject, + )?; assert_diagnostics!(snapshot, messages); Ok(()) } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs index dff668d028..f97743c2dc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs @@ -88,46 +88,21 @@ pub(crate) fn invalid_index_type(checker: &Checker, expr: &ExprSubscript) { return; }; - if index_type.is_literal() { - // If the index is a literal, require an integer - if index_type != CheckableExprType::IntLiteral - && index_type != CheckableExprType::BooleanLiteral - { - checker.report_diagnostic( - InvalidIndexType { - value_type: value_type.to_string(), - index_type: index_type.to_string(), - is_slice: false, - }, - index.range(), - ); - } - } else if let Expr::Slice(ExprSlice { + if let Expr::Slice(ExprSlice { lower, upper, step, .. }) = index.as_ref() { for is_slice in [lower, upper, step].into_iter().flatten() { let Some(is_slice_type) = CheckableExprType::try_from(is_slice) else { - return; + continue; }; - if is_slice_type.is_literal() { - // If the index is a slice, require integer or null bounds - if !matches!( - is_slice_type, - CheckableExprType::IntLiteral - | CheckableExprType::NoneLiteral - | CheckableExprType::BooleanLiteral - ) { - checker.report_diagnostic( - InvalidIndexType { - value_type: value_type.to_string(), - index_type: is_slice_type.to_string(), - is_slice: true, - }, - is_slice.range(), - ); - } - } else if let Some(is_slice_type) = CheckableExprType::try_from(is_slice.as_ref()) { + // A slice bound must be an integer, `None`, or a boolean. + if !matches!( + is_slice_type, + CheckableExprType::IntLiteral + | CheckableExprType::NoneLiteral + | CheckableExprType::BooleanLiteral + ) { checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), @@ -138,8 +113,11 @@ pub(crate) fn invalid_index_type(checker: &Checker, expr: &ExprSubscript) { ); } } - } else { - // If it's some other checkable data type, it's a violation + } else if !matches!( + index_type, + CheckableExprType::IntLiteral | CheckableExprType::BooleanLiteral + ) { + // A non-slice index must be an integer or a boolean. checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), @@ -235,18 +213,4 @@ impl CheckableExprType { _ => None, } } - - fn is_literal(self) -> bool { - matches!( - self, - Self::StringLiteral - | Self::BytesLiteral - | Self::IntLiteral - | Self::FloatLiteral - | Self::ComplexLiteral - | Self::BooleanLiteral - | Self::NoneLiteral - | Self::EllipsisLiteral - ) - } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs index 4da7ce2996..3c64e79ce6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs @@ -1,12 +1,18 @@ +use pyproject_toml::PyProjectToml; +use serde::Deserialize; +use toml::Spanned; +use toml::de::DeTable; + use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_text_size::{TextRange, TextSize}; -use crate::{FixAvailability, Violation}; +use crate::{FixAvailability, Violation, checkers::ast::LintContext}; /// ## What it does /// Checks for any pyproject.toml that does not conform to the schema from the relevant PEPs. /// /// ## Why is this bad? -/// Your project may contain invalid metadata or configuration without you noticing +/// Your project may contain invalid metadata or configuration without you noticing. /// /// ## Example /// ```toml @@ -45,3 +51,34 @@ impl Violation for InvalidPyprojectToml { format!("Failed to parse pyproject.toml: {message}") } } + +/// RUF200 +pub(crate) fn invalid_pyproject_toml( + context: &LintContext, + document: Result>, toml::de::Error>, +) { + let err = match document { + Ok(document) => { + let deserializer = toml::de::Deserializer::from(document); + let Err(mut err) = PyProjectToml::deserialize(deserializer) else { + return; + }; + err.set_input(Some(context.source_file().source_text())); + err + } + Err(err) => err, + }; + + let range = match err.span() { + // This is bad but sometimes toml and/or serde just don't give us spans + // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 + None => TextRange::default(), + Some(range) => TextRange::new( + TextSize::try_from(range.start).unwrap(), + TextSize::try_from(range.end).unwrap(), + ), + }; + + let toml_err = err.message().to_string(); + context.report_diagnostic(InvalidPyprojectToml { message: toml_err }, range); +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 09e55de4f7..1b2aeca17b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -40,6 +40,7 @@ pub(crate) use never_union::*; pub(crate) use non_empty_init_module::*; pub(crate) use non_octal_permissions::*; pub(crate) use none_not_at_end_of_union::*; +pub(crate) use noqa_comments::*; pub(crate) use os_path_commonprefix::*; pub(crate) use parenthesize_chained_operators::*; pub(crate) use post_init_default::*; @@ -49,6 +50,8 @@ pub(crate) use pytest_raises_ambiguous_pattern::*; pub(crate) use quadratic_list_summation::*; pub(crate) use redirected_noqa::*; pub(crate) use redundant_bool_literal::*; +pub(crate) use rule_codes_in_selectors::*; +pub(crate) use rule_codes_in_suppression_comments::*; pub(crate) use sort_dunder_all::*; pub(crate) use sort_dunder_slots::*; pub(crate) use starmap_zip::*; @@ -118,6 +121,7 @@ mod never_union; mod non_empty_init_module; mod non_octal_permissions; mod none_not_at_end_of_union; +mod noqa_comments; mod os_path_commonprefix; mod parenthesize_chained_operators; mod post_init_default; @@ -127,6 +131,8 @@ mod pytest_raises_ambiguous_pattern; mod quadratic_list_summation; mod redirected_noqa; mod redundant_bool_literal; +mod rule_codes_in_selectors; +mod rule_codes_in_suppression_comments; mod sequence_sorting; mod sort_dunder_all; mod sort_dunder_slots; diff --git a/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs new file mode 100644 index 0000000000..d01abd0e6d --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs @@ -0,0 +1,201 @@ +use itertools::Itertools; + +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + FixAvailability, Locator, Violation, checkers::ast::LintContext, codes::Rule, noqa::Directive, + suppression::Suppressions, +}; + +/// ## What it does +/// +/// Checks for the use of `noqa` comments instead of Ruff-specific `ruff:ignore` comments. +/// +/// ## Why is this bad? +/// +/// `ruff:ignore` comments allow the use of rule names instead of codes and can be used in more +/// places than `noqa` comments. +/// +/// Note that this is an opinionated, stylistic rule. `noqa` comments may be needed for backwards +/// compatibility with other tools. You should also feel free to disable this rule if you simply +/// prefer `noqa` comments. +/// +/// ## Example +/// +/// ```python +/// import os # noqa: F401 +/// ``` +/// +/// Use instead: +/// ```python +/// import os # ruff:ignore[F401] +/// ``` +/// +/// Or if you prefer the own-line form: +/// +/// ```python +/// # ruff:ignore[unused-import] +/// import os +/// ``` +/// +/// ## Options +/// +/// This rule will flag `noqa` comments containing rule codes that are unknown to Ruff, even if they +/// are valid for other tools. You can tell Ruff to ignore such codes by configuring the list of +/// known "external" rule codes with the following option: +/// +/// - `lint.external` +/// +/// Ruff will still emit a diagnostic without a fix if `external` and known codes are present in the +/// same `noqa` comment, assuming that only the `external` codes need to remain in the `noqa` +/// comment. +/// +/// ## See also +/// +/// This rule avoids offering a fix if any of the rule codes in a `noqa` comment are unused. See +/// `unused-noqa` for a rule that will remove these and allow the remaining codes to be moved into a +/// `ruff:ignore` comment. +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.15.22")] +pub(crate) struct NoqaComments { + file_level: bool, +} + +impl Violation for NoqaComments { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + if !self.file_level { + "`noqa` comment used instead of `ruff:ignore`".to_string() + } else { + "`ruff: noqa` comment used instead of `ruff:file-ignore`".to_string() + } + } + + fn fix_title(&self) -> Option { + Some(if self.file_level { + "Use `ruff:file-ignore` instead".to_string() + } else { + "Use `ruff:ignore` instead".to_string() + }) + } +} + +/// RUF105 +pub(crate) fn noqa_comments( + context: &LintContext, + locator: &Locator, + file_level: bool, + has_unused_codes: bool, + directive: &Directive, + matches: &[Rule], + suppressions: &Suppressions, +) { + let codes = Codes::from_directive(directive, matches); + + let range = codes.range; + + if file_level && locator.slice(range).contains("flake8") { + return; + } + + let has_external_codes = if let CodesKind::Codes(codes) = codes.kind { + let external_codes = codes + .iter() + .filter(|code| { + context + .settings() + .external + .iter() + .any(|prefix| code.as_str().starts_with(prefix)) + }) + .count(); + + // Avoid a diagnostic if all of the codes are external. + if external_codes == codes.len() { + return; + } + + external_codes > 0 + } else { + false + }; + + if suppressions.check_rule(Rule::NoqaComments, range, None) { + return; + } + + let mut diagnostic = context.report_diagnostic(NoqaComments { file_level }, range); + + // If some codes are external, return without a fix. + if has_external_codes { + return; + } + + // Similarly, return without a fix if any unused codes are present. This avoids potentially + // activating an unused `noqa` comment on its own line like: + // + // ```py + // # noqa: F401 + // import math + // ``` + // + // by converting it to a valid `ruff:ignore` comment. + if has_unused_codes { + return; + } + + let edit = Edit::range_replacement( + format!( + "# ruff:{action}[{codes}]", + action = if file_level { "file-ignore" } else { "ignore" }, + ), + codes.range, + ); + diagnostic.set_fix(Fix::safe_edit(edit)); +} + +struct Codes<'a> { + kind: CodesKind<'a>, + range: TextRange, +} + +enum CodesKind<'a> { + Codes(&'a crate::noqa::Codes<'a>), + Rules(&'a [Rule]), +} + +impl<'a> Codes<'a> { + fn from_directive(directive: &'a Directive, matches: &'a [Rule]) -> Self { + let kind = match directive { + Directive::All(_) => CodesKind::Rules(matches), + Directive::Codes(codes) => CodesKind::Codes(codes), + }; + + Self { + kind, + range: directive.range(), + } + } +} + +impl std::fmt::Display for Codes<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + CodesKind::Codes(codes) => write!(f, "{}", codes.iter().join(", ")), + CodesKind::Rules(rules) => write!( + f, + "{}", + rules + .iter() + .map(Rule::noqa_code) + .sorted() + .dedup() + .join(", ") + ), + } + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs new file mode 100644 index 0000000000..9179f982c8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -0,0 +1,223 @@ +use toml::Spanned; +use toml::de::{DeArray, DeTable, DeValue}; + +use ruff_db::diagnostic::LintName; +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::TomlSourceType; +use ruff_text_size::{TextLen, TextRange, TextSize}; + +use crate::{ + AlwaysFixableViolation, checkers::ast::LintContext, codes::Rule, + preview::is_human_readable_names_enabled, rule_redirects::get_redirect_target, +}; + +/// ## What it does +/// +/// Checks for any configuration files that use rule codes as selectors. +/// +/// ## Why is this bad? +/// +/// Human-readable rule names are easier to understand than rule codes. Using names also avoids +/// requiring readers to look up the meaning of each code. +/// +/// ## Example +/// +/// ```toml +/// [tool.ruff.lint] +/// select = ["F401"] +/// ``` +/// +/// Use instead: +/// +/// ```toml +/// [tool.ruff.lint] +/// select = ["unused-import"] +/// ``` +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.15.22")] +pub(crate) struct RuleCodesInSelectors { + selector: &'static str, + name: &'static str, + in_lint_table: bool, +} + +impl AlwaysFixableViolation for RuleCodesInSelectors { + #[derive_message_formats] + fn message(&self) -> String { + let Self { + selector, + in_lint_table, + name: _, + } = self; + if *in_lint_table { + format!("Rule code used instead of name in `lint.{selector}`") + } else { + format!("Rule code used instead of name in `{selector}`") + } + } + + fn fix_title(&self) -> String { + format!("Replace rule code with `{name}`", name = self.name) + } +} + +/// RUF201 +pub(crate) fn rule_codes_in_selectors( + context: &LintContext, + document: &DeTable<'_>, + source_type: TomlSourceType, +) { + if !is_human_readable_names_enabled(context.settings().preview) { + return; + } + + let ruff = match source_type { + TomlSourceType::Pyproject => document + .get("tool") + .and_then(|tool| tool.get_ref().get("ruff")) + .and_then(|ruff| ruff.get_ref().as_table()), + TomlSourceType::Ruff => Some(document), + _ => None, + }; + + let Some(ruff) = ruff else { + return; + }; + + check_selectors(context, ruff, false); + + if let Some(lint) = ruff.get("lint").and_then(|lint| lint.get_ref().as_table()) { + check_selectors(context, lint, true); + } +} + +/// Selectors that are themselves arrays. +/// +/// For example: +/// +/// ```toml +/// select = ["F401"] +/// ``` +const ARRAY_SELECTORS: &[&str] = &[ + "select", + "extend-select", + "fixable", + "extend-fixable", + "ignore", + "extend-ignore", + "unfixable", + "extend-unfixable", + "extend-safe-fixes", + "extend-unsafe-fixes", +]; + +/// Selectors that are tables containing arrays. +/// +/// For example: +/// +/// ```toml +/// per-file-ignores = { "*.py" = ["F401"] } +/// ``` +const TABLE_SELECTORS: &[&str] = &["per-file-ignores", "extend-per-file-ignores"]; + +fn check_selectors(context: &LintContext, table: &DeTable<'_>, in_lint_table: bool) { + for &selector in ARRAY_SELECTORS { + let Some(value) = table.get(selector) else { + continue; + }; + + if let DeValue::Array(values) = value.get_ref() { + check_selector_array(context, values, selector, in_lint_table); + } + } + + for &selector in TABLE_SELECTORS { + let Some(value) = table.get(selector) else { + continue; + }; + + if let DeValue::Table(per_file) = value.get_ref() { + for value in per_file.values() { + let Some(values) = value.get_ref().as_array() else { + continue; + }; + check_selector_array(context, values, selector, in_lint_table); + } + } + } +} + +fn check_selector_array( + context: &LintContext, + values: &DeArray<'_>, + selector: &'static str, + in_lint_table: bool, +) { + let source = context.source_file().source_text(); + + for value in values { + let Some(RuleCode { name, range }) = RuleCode::from_spanned(value, source) else { + continue; + }; + + context + .report_diagnostic( + RuleCodesInSelectors { + selector, + in_lint_table, + name: name.as_str(), + }, + range, + ) + .set_fix(Fix::safe_edit(Edit::range_replacement( + name.to_string(), + range, + ))); + } +} + +struct RuleCode { + name: LintName, + range: TextRange, +} + +impl RuleCode { + /// Extract a rule code and its range from a spanned TOML string. + /// + /// The range corresponds to the code itself rather than the surrounding string: + /// + /// ```toml + /// [lint] + /// select = ["F401"] + /// ^^^^ + /// ``` + fn from_spanned(spanned: &Spanned>, source: &str) -> Option { + let code = spanned.get_ref().as_str()?; + let code = get_redirect_target(code).unwrap_or(code); + let rule = Rule::from_code(code).ok()?; + + let span = spanned.span(); + let range = TextRange::new( + TextSize::try_from(span.start).unwrap(), + TextSize::try_from(span.end).unwrap(), + ); + + // Note that this should be infallible because the parsed TOML string is surrounded by valid + // quotes, and `Rule::from_code` above guarantees that its content is a valid rule code. This + // means that we don't have to worry about stripping nested quotes like `"'F401'"` or similar. + let range = { + let string = &source[range]; + let content = string.trim_start_matches(['"', '\'']); + let quote_len = string.text_len() - content.text_len(); + let start = range.start() + quote_len; + let end = range.end() - quote_len; + TextRange::new(start, end) + }; + + Some(Self { + name: rule.name(), + range, + }) + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs new file mode 100644 index 0000000000..53d42e3cc5 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs @@ -0,0 +1,40 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::AlwaysFixableViolation; + +/// ## What it does +/// +/// Checks for rule codes in Ruff-specific suppression comments. +/// +/// ## Why is this bad? +/// +/// Human-readable rule names are easier to understand than rule codes. Using names also avoids +/// requiring readers to look up the meaning of each code. +/// +/// This rule applies to `ruff:ignore`, `ruff:file-ignore`, `ruff:disable`, and `ruff:enable` +/// comments. +/// +/// ## Example +/// +/// ```python +/// import os # ruff:ignore[F401] +/// ``` +/// +/// Use instead: +/// ```python +/// import os # ruff:ignore[unused-import] +/// ``` +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.15.22")] +pub(crate) struct RuleCodesInSuppressionComments; + +impl AlwaysFixableViolation for RuleCodesInSuppressionComments { + #[derive_message_formats] + fn message(&self) -> String { + "Rule code used instead of name in suppression comment".to_string() + } + + fn fix_title(&self) -> String { + "Replace rule code with name".to_string() + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs index 4038a9508a..0fcd0ca5fb 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs @@ -240,13 +240,9 @@ impl<'a> StringLiteralDisplay<'a> { ast::Expr::Dict(dict) => { let mut narrowed_keys = Vec::with_capacity(dict.len()); for key in dict.iter_keys() { - if let Some(key) = key { - // This is somewhat unfortunate, - // *but* using a dict for __slots__ is very rare - narrowed_keys.push(key.to_owned()); - } else { - return None; - } + // This is somewhat unfortunate, + // *but* using a dict for __slots__ is very rare + narrowed_keys.push(key?.to_owned()); } // If `None` was present in the keys, it indicates a "** splat", .e.g // `__slots__ = {"foo": "bar", **other_dict}` diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs index 542a42055e..2b898822de 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs @@ -18,7 +18,11 @@ use crate::rules::fastapi::rules::is_fastapi_route; /// ## Why is this bad? /// Declaring a function `async` when it's not is usually a mistake, and will artificially limit the /// contexts where that function may be called. In some cases, labeling a function `async` is -/// semantically meaningful (e.g. with the trio library). +/// semantically meaningful. For example, an async test or callback may need to run in an async +/// execution context, even if it only uses a `ContextVar`. +/// +/// If the async context is intentional, add an actual await expression, such as +/// `await asyncio.sleep(0)`, or disable this rule for the function. /// /// ## Example /// ```python diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap index 22917b601a..089bac11e0 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap @@ -442,4 +442,15 @@ RUF016 Indexed access to type `list` uses type `generator` instead of an integer 129 | # Should emit for invalid access using generator 130 | var = [1, 2, 3][(x for x in ())] | ^^^^^^^^^^^^^^^ +131 | +132 | # Should still emit for a later invalid bound, even if an earlier bound is unrecognized + | + +RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer + --> RUF016.py:134:19 + | +132 | # Should still emit for a later invalid bound, even if an earlier bound is unrecognized +133 | x = "x" +134 | var = [1, 2, 3][x:"y"] + | ^^^ | diff --git a/crates/ruff_linter/src/source_kind.rs b/crates/ruff_linter/src/source_kind.rs index 1ac4121223..31d5e22574 100644 --- a/crates/ruff_linter/src/source_kind.rs +++ b/crates/ruff_linter/src/source_kind.rs @@ -235,6 +235,15 @@ pub struct SourceKindDiff<'a> { path: Option<&'a Path>, } +impl<'a> SourceKindDiff<'a> { + pub fn from_text(original: &'a str, modified: &'a str, path: Option<&'a Path>) -> Self { + Self { + kind: DiffKind::Text(original, modified), + path, + } + } +} + impl std::fmt::Display for SourceKindDiff<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind { @@ -265,8 +274,8 @@ impl std::fmt::Display for SourceKindDiff<'_> { || (format!("cell {idx}"), format!("cell {idx}")), |path| { ( - format!("{}:cell {}", &fs::relativize_path(path), idx), - format!("{}:cell {}", &fs::relativize_path(path), idx), + format!("{}:cell {}", fs::relativize_path(path), idx), + format!("{}:cell {}", fs::relativize_path(path), idx), ) }, ); diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index d6f2d9059c..01fe547eb9 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -23,7 +23,8 @@ use crate::preview::{is_human_readable_names_enabled, is_ruff_ignore_enabled}; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{ InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind, - UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, UnusedNOQAKind, code_is_valid, + RuleCodesInSuppressionComments, UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, + UnusedNOQAKind, code_is_valid, }; use crate::settings::LinterSettings; use crate::settings::types::PreviewMode; @@ -345,22 +346,49 @@ impl Suppressions { return false; }; + self.check_suppression( + diagnostic.secondary_code(), + diagnostic.name(), + range, + diagnostic.parent(), + ) + } + + /// Check whether a rule is suppressed at the given range and mark the suppression as used. + pub(crate) fn check_rule( + &self, + rule: Rule, + range: TextRange, + parent: Option, + ) -> bool { + self.check_suppression(Some(&rule.noqa_code()), rule.name().as_str(), range, parent) + } + + /// Check whether the given rule code or name corresponds to a valid suppression comment at + /// `range` itself or the `parent` offset. + fn check_suppression( + &self, + code: Option<&C>, + name: &str, + range: TextRange, + parent: Option, + ) -> bool + where + C: for<'a> PartialEq<&'a str>, + { for suppression in &self.valid { let suppression_code = get_redirect_target(suppression.code.as_str()).unwrap_or(suppression.code.as_str()); - let code_matches = diagnostic - .secondary_code() - .is_some_and(|code| *code == suppression_code); - - let name_matches = is_human_readable_names_enabled(self.preview) - && diagnostic.name() == suppression_code; + let code_matches = code.is_some_and(|code| code == &suppression_code); + let name_matches = + is_human_readable_names_enabled(self.preview) && name == suppression_code; if !code_matches && !name_matches { continue; } - if suppression.applies_to_diagnostic(range, diagnostic.parent()) { + if suppression.applies_to_diagnostic(range, parent) { suppression.used.set(true); return true; } @@ -368,6 +396,49 @@ impl Suppressions { false } + /// Check for rule codes in valid suppression comments. + pub(crate) fn check_rule_codes(&self, context: &LintContext, locator: &Locator) { + if !context.is_rule_enabled(Rule::RuleCodesInSuppressionComments) { + return; + } + + // Each comment or matched pair produces one valid suppression per code, all sharing the + // same first comment range. + let mut seen_comments = FxHashSet::default(); + + for suppression in &self.valid { + let first_comment = suppression.comments.first(); + if !seen_comments.insert(first_comment.range) { + continue; + } + + let second_comment = suppression.comments.second(); + for (index, range) in first_comment.codes.iter().enumerate() { + let original = locator.slice(range); + let code = get_redirect_target(original).unwrap_or(original); + let Ok(rule) = Rule::from_code(code) else { + continue; + }; + + let mut diagnostic = + context.report_diagnostic(RuleCodesInSuppressionComments, *range); + let name = rule.name().to_string(); + let fix = if let Some(second_range) = + second_comment.and_then(|comment| comment.codes.get(index)) + { + diagnostic.secondary_annotation_without_message(*second_range); + Fix::safe_edits( + Edit::range_replacement(name.clone(), *range), + [Edit::range_replacement(name, *second_range)], + ) + } else { + Fix::safe_edit(Edit::range_replacement(name, *range)) + }; + diagnostic.set_fix(fix); + } + } + } + pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) { fn process_pending_diagnostics( key: Option, diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 1ca8cc9d91..230776646a 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -134,6 +134,24 @@ pub(crate) fn test_path( Ok(test_contents(&source_kind, &path, settings).0) } +/// Run the configuration TOML linter on a file in the `resources/test/fixtures` directory. +#[cfg(test)] +pub(crate) fn test_toml_path( + path: impl AsRef, + settings: &LinterSettings, + source_type: ruff_python_ast::TomlSourceType, +) -> Result> { + let path = test_resource_path("fixtures").join(path); + let filename = path.file_name().unwrap_or_else(|| path.as_os_str()); + let contents = std::fs::read_to_string(&path)?; + Ok(crate::toml::lint_toml( + Path::new(filename), + &contents, + settings, + source_type, + )) +} + /// Test a file with two different settings and return the differences #[cfg(test)] pub(crate) fn test_path_with_settings_diff( diff --git a/crates/ruff_linter/src/toml.rs b/crates/ruff_linter/src/toml.rs new file mode 100644 index 0000000000..4a0fa817ee --- /dev/null +++ b/crates/ruff_linter/src/toml.rs @@ -0,0 +1,89 @@ +use std::borrow::Cow; +use std::path::Path; + +use toml::de::DeTable; + +use ruff_db::diagnostic::Diagnostic; +use ruff_python_ast::TomlSourceType; + +use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::fix::{FixResult, fix_file}; +use crate::linter::{FixTable, MAX_ITERATIONS, report_failed_to_converge_error}; +use crate::registry::Rule; +use crate::rules::ruff::rules::{invalid_pyproject_toml, rule_codes_in_selectors}; +use crate::settings::LinterSettings; +use crate::settings::types::UnsafeFixes; + +pub struct TomlFixerResult<'a> { + pub diagnostics: Vec, + pub transformed: Cow<'a, str>, + pub fixed: FixTable, +} + +pub fn lint_toml( + path: &Path, + contents: &str, + settings: &LinterSettings, + source_type: TomlSourceType, +) -> Vec { + let context = LintContext::new(path, contents, settings); + + let document = DeTable::parse(contents); + + if context.is_rule_enabled(Rule::RuleCodesInSelectors) + && let Ok(document) = &document + { + rule_codes_in_selectors(&context, document.get_ref(), source_type); + } + + if source_type.is_pyproject() && context.is_rule_enabled(Rule::InvalidPyprojectToml) { + invalid_pyproject_toml(&context, document); + } + + context.into_diagnostics() +} + +/// Generate [`Diagnostic`]s for a TOML configuration file, iteratively fixing until stable. +pub fn lint_fix_toml<'a>( + path: &Path, + source: &'a str, + settings: &LinterSettings, + source_type: TomlSourceType, + unsafe_fixes: UnsafeFixes, +) -> TomlFixerResult<'a> { + let mut diagnostics = lint_toml(path, source, settings, source_type); + let mut transformed = Cow::Borrowed(source); + let mut fixed = FixTable::default(); + let mut iterations = 0; + + loop { + let locator = Locator::new(transformed.as_ref()); + let Some(FixResult { code, fixes, .. }) = fix_file(&diagnostics, &locator, unsafe_fixes) + else { + return TomlFixerResult { + diagnostics, + transformed, + fixed, + }; + }; + + if iterations >= MAX_ITERATIONS { + report_failed_to_converge_error(path, transformed.as_ref(), &diagnostics); + return TomlFixerResult { + diagnostics, + transformed, + fixed, + }; + } + + for (rule, name, count) in fixes.iter() { + *fixed.entry(rule).or_default(name) += count; + } + + transformed = Cow::Owned(code); + iterations += 1; + + diagnostics = lint_toml(path, transformed.as_ref(), settings, source_type); + } +} diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 94305e6c55..3b0320b491 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index aad888cbe8..127dbd027c 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_macros). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index ef8731bee7..e7106dcf34 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index 752b559894..f6007c1178 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_markdown). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_mdtest/src/lib.rs b/crates/ruff_mdtest/src/lib.rs index acdfcfd2f7..3082d15a4c 100644 --- a/crates/ruff_mdtest/src/lib.rs +++ b/crates/ruff_mdtest/src/lib.rs @@ -12,6 +12,8 @@ use ruff_db::source::source_text; use ruff_db::system::{DbWithWritableSystem as _, SystemPathBuf}; use ruff_linter::source_kind::SourceKind; use ruff_linter::test::test_contents; +use ruff_linter::toml::lint_toml; +use ruff_python_ast::SourceType; use ruff_ranged_value::{ValueSource, ValueSourceGuard}; use ruff_workspace::configuration::Configuration; use ruff_workspace::options::Options; @@ -68,8 +70,8 @@ fn run_test( } assert!( - matches!(embedded.lang, "py" | "pyi" | "python" | "ipynb"), - "Supported file types are: py (or python), pyi, ipynb, and ignore" + matches!(embedded.lang, "py" | "pyi" | "python" | "ipynb" | "toml"), + "Supported file types are: py (or python), pyi, ipynb, toml, and ignore" ); let full_path = embedded.full_path(&project_root); @@ -80,7 +82,7 @@ fn run_test( Some(TestFile { file, - code_blocks: embedded.python_code_blocks.clone(), + code_blocks: embedded.code_blocks.clone(), }) }) .collect(); @@ -107,21 +109,29 @@ fn run_test( let mdtest_result = attempt_test( |file| { let source = source_text(db, file); - let source_kind = if let Some(notebook) = source.as_notebook() { - SourceKind::ipy_notebook(notebook.clone()) - } else { - SourceKind::Python { - code: source.as_str().to_string(), - is_stub: file.is_stub(db), - is_basedpython: file.source_type(db).is_basedpython(), - } - }; let path = file .path(db) .as_system_path() .expect("mdtest files are on the system") .as_std_path(); - test_contents(&source_kind, path, &settings.linter).0 + match SourceType::from(path) { + SourceType::Python(_) => { + let source_kind = if let Some(notebook) = source.as_notebook() { + SourceKind::ipy_notebook(notebook.clone()) + } else { + SourceKind::Python { + code: source.as_str().to_string(), + is_stub: file.is_stub(db), + is_basedpython: file.source_type(db).is_basedpython(), + } + }; + test_contents(&source_kind, path, &settings.linter).0 + } + SourceType::Toml(source_type) => { + lint_toml(path, source.as_str(), &settings.linter, source_type) + } + SourceType::Markdown => Vec::new(), + } }, test_file, ); diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index 2116532457..5758dc8979 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index 64a49b411d..75700ae5d3 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_memory_usage). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index da17f6cb12..c5216cb7e2 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -16,7 +16,6 @@ ruff_source_file = { workspace = true, features = ["serde"] } ruff_text_size = { workspace = true } anyhow = { workspace = true } -itertools = { workspace = true } rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index 1721084d3e..1f01efbdc5 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_notebook). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/src/cell.rs b/crates/ruff_notebook/src/cell.rs index 1c7b749e05..51beeba55d 100644 --- a/crates/ruff_notebook/src/cell.rs +++ b/crates/ruff_notebook/src/cell.rs @@ -1,8 +1,6 @@ use std::fmt; use std::ops::{Deref, DerefMut}; -use itertools::Itertools; - use ruff_text_size::{TextRange, TextSize}; use crate::schema::{Cell, SourceValue}; @@ -284,7 +282,7 @@ impl CellOffsets { /// Returns the range of the cell containing the given offset, if any. pub fn containing_range(&self, offset: TextSize) -> Option { - self.iter().tuple_windows().find_map(|(start, end)| { + self.array_windows::<2>().find_map(|[start, end]| { if *start <= offset && offset < *end { Some(TextRange::new(*start, *end)) } else { @@ -325,9 +323,8 @@ impl CellOffsets { /// Returns an iterator over [`TextRange`]s covered by each cell. pub fn ranges(&self) -> impl Iterator { - self.iter() - .tuple_windows() - .map(|(start, end)| TextRange::new(*start, *end)) + self.array_windows::<2>() + .map(|[start, end]| TextRange::new(*start, *end)) } /// Returns an iterator over the concatenated source ranges covered by each cell's actual diff --git a/crates/ruff_notebook/src/notebook.rs b/crates/ruff_notebook/src/notebook.rs index 62889dbf9d..76f8fa854f 100644 --- a/crates/ruff_notebook/src/notebook.rs +++ b/crates/ruff_notebook/src/notebook.rs @@ -1,4 +1,3 @@ -use itertools::Itertools; use rand::{RngExt, SeedableRng}; use serde::Serialize; use serde_json::error::Category; @@ -303,10 +302,10 @@ impl Notebook { fn update_cell_content(&mut self, transformed: &str) -> bool { let mut missing_separator = false; - for (&idx, (start, end)) in self + for (&idx, &[start, end]) in self .valid_code_cells .iter() - .zip(self.cell_offsets.iter().tuple_windows::<(_, _)>()) + .zip(self.cell_offsets.array_windows::<2>()) { let cell_content = transformed .get(start.to_usize()..end.to_usize()) diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index 7d2e973c01..4e6b438efe 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 25383ceb42..4a425586f4 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_options_metadata). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index c64a942561..f838d6cc4d 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -26,6 +26,7 @@ ruff_text_size = { workspace = true } aho-corasick = { workspace = true } arrayvec = { workspace = true } bitflags = { workspace = true } +char_str = { workspace = true } compact_str = { workspace = true } get-size2 = { workspace = true, optional = true } is-macro = { workspace = true } @@ -45,10 +46,12 @@ serde = [ "dep:serde", "ruff_text_size/serde", "dep:ruff_cache", + "char_str/serde", "compact_str/serde", "thin-vec/serde", ] -get-size = ["dep:get-size2", "ruff_text_size/get-size"] +get-size = ["dep:get-size2", "char_str/get-size", "ruff_text_size/get-size"] +salsa = ["dep:salsa"] [lints] workspace = true diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 7cf1b68d16..246cbdc200 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_ast). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index d65f6f281f..ff42a95878 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -312,18 +312,10 @@ where range: _, node_index: _, }) - | Expr::List(ast::ExprList { - elts, - range: _, - node_index: _, - .. - }) - | Expr::Tuple(ast::ExprTuple { - elts, - range: _, - node_index: _, - .. - }) => elts.iter().any(|expr| any_over_expr(expr, &mut *func)), + | Expr::List(ast::ExprList { elts, .. }) + | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + elts.iter().any(|expr| any_over_expr(expr, &mut *func)) + } Expr::ListComp(ast::ExprListComp { elt, generators, @@ -383,18 +375,8 @@ where range: _, node_index: _, }) - | Expr::Attribute(ast::ExprAttribute { - value, - range: _, - node_index: _, - .. - }) - | Expr::Starred(ast::ExprStarred { - value, - range: _, - node_index: _, - .. - }) => any_over_expr(value, func), + | Expr::Attribute(ast::ExprAttribute { value, .. }) + | Expr::Starred(ast::ExprStarred { value, .. }) => any_over_expr(value, func), Expr::Yield(ast::ExprYield { value, range: _, diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 73370eed3a..03f1393d47 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -69,6 +69,9 @@ impl> From

for SourceType { Some(filename) if filename == "pyproject.toml" => Self::Toml(TomlSourceType::Pyproject), Some(filename) if filename == "Pipfile" => Self::Toml(TomlSourceType::Pipfile), Some(filename) if filename == "poetry.lock" => Self::Toml(TomlSourceType::Poetry), + Some(filename) if filename == "ruff.toml" || filename == ".ruff.toml" => { + Self::Toml(TomlSourceType::Ruff) + } _ => Self::from_extension( path.as_ref() .extension() @@ -83,6 +86,8 @@ impl> From

for SourceType { pub enum TomlSourceType { /// The source is a `pyproject.toml`. Pyproject, + /// The source is a `ruff.toml` or `.ruff.toml`. + Ruff, /// The source is a `Pipfile`. Pipfile, /// The source is a `poetry.lock`. diff --git a/crates/ruff_python_ast/src/name.rs b/crates/ruff_python_ast/src/name.rs index da9730dab2..cc4b20f570 100644 --- a/crates/ruff_python_ast/src/name.rs +++ b/crates/ruff_python_ast/src/name.rs @@ -4,48 +4,97 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use arrayvec::ArrayVec; +use char_str::{CharStr, CharString}; use crate::Expr; use crate::generated::ExprName; +/// An immutable name. +/// +/// # Choosing a string representation +/// +/// On 64-bit targets, [`CharStr`] occupies 16 bytes and stores up to 16 UTF-8 bytes inline. Longer +/// values use an exactly-sized, reference-counted allocation, so cloning a heap-backed value +/// reuses its allocation. [`compact_str::CompactString`] occupies 24 bytes, stores up to 24 bytes +/// inline, and remains mutable; cloning a heap-backed value copies its contents into a new +/// allocation. +/// +/// Prefer `CharStr` for immutable text that is retained densely or passed between owners, when +/// either the smaller handle or structural sharing offsets the extra heap allocations for values +/// between 17 and 24 bytes. Prefer `CompactString` for uniquely owned text, especially when it is +/// built incrementally, mutated, or commonly falls in that 17-to-24-byte range. +/// +/// `Name` uses `CharStr` because names appear throughout the AST and repeated heap-backed parser +/// names share an allocation. By contrast, [`crate::DebugText`] uses `CompactString` because it +/// builds a uniquely owned buffer incrementally, and `ty_module_resolver::ModuleName` uses +/// `CompactString` because module names can be extended in place. +/// +/// Converting a borrowed `&str` into `CharStr` creates a new value and does not preserve structural +/// sharing. When an API retains text already held in a `CharStr` (including a `Name`), pass or clone +/// the owned value rather than converting it through `&str`. This is especially relevant at Salsa +/// interning boundaries. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[cfg_attr(feature = "salsa", derive(salsa::SalsaValue))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))] -#[cfg_attr(feature = "salsa", derive(salsa::Update))] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] #[cfg_attr( feature = "schemars", derive(schemars::JsonSchema), schemars(with = "String") )] -pub struct Name(compact_str::CompactString); +pub struct Name(CharStr); impl Name { #[inline] pub fn empty() -> Self { - Self(compact_str::CompactString::default()) + Self(CharStr::new()) } #[inline] pub fn new(name: impl AsRef) -> Self { - Self(compact_str::CompactString::new(name)) + Self(CharStr::from(name.as_ref())) + } + + /// Creates an inline name, returning `None` if `name` does not fit inline. + #[inline] + pub fn new_inline(name: impl AsRef) -> Option { + CharStr::new_inline(name.as_ref()).map(Self) + } + + /// Creates an exactly-sized, heap-allocated name. + #[inline] + pub fn new_heap(name: impl AsRef) -> Self { + Self(CharStr::new_heap(name.as_ref())) } #[inline] pub const fn new_static(name: &'static str) -> Self { - Self(compact_str::CompactString::const_new(name)) + Self(CharStr::from_static_str(name)) } - pub fn shrink_to_fit(&mut self) { - self.0.shrink_to_fit(); + /// Creates an exactly-sized name by concatenating string slices. + /// + /// The combined length is computed up front, so heap storage is allocated at most once. + #[inline] + pub fn concat>(slices: &[T]) -> Self { + Self(CharStr::concat(slices)) } - pub fn as_str(&self) -> &str { - self.0.as_str() + /// Creates an exactly-sized name by joining string slices with a separator. + /// + /// Like [`Name::concat`], this computes the combined length up front, so heap storage is + /// allocated at most once. For dynamically formatted names, use [`Name::from`] with + /// [`format_char!`](char_str::format_char). If a [`CharStr`] is sufficient, use + /// [`format_char_str!`](char_str::format_char_str) instead. + #[inline] + pub fn join>(slices: &[T], separator: &str) -> Self { + Self(CharStr::join(slices, separator)) } - pub fn push_str(&mut self, s: &str) { - self.0.push_str(s); + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() } } @@ -55,13 +104,6 @@ impl Debug for Name { } } -impl std::fmt::Write for Name { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.0.push_str(s); - Ok(()) - } -} - impl AsRef for Name { #[inline] fn as_ref(&self) -> &str { @@ -88,7 +130,7 @@ impl Borrow for Name { impl<'a> From<&'a str> for Name { #[inline] fn from(s: &'a str) -> Self { - Name(s.into()) + Name::new(s) } } @@ -102,7 +144,7 @@ impl From for Name { impl<'a> From<&'a String> for Name { #[inline] fn from(s: &'a String) -> Self { - Name(s.into()) + Name::new(s) } } @@ -120,14 +162,35 @@ impl From> for Name { } } -impl From for Name { +#[cfg(feature = "salsa")] +impl salsa::Lookup for &str { #[inline] - fn from(value: compact_str::CompactString) -> Self { - Self(value) + fn into_owned(self) -> Name { + Name::new(self) } } -impl From for compact_str::CompactString { +#[cfg(feature = "salsa")] +impl salsa::HashEqLike<&str> for Name { + #[inline] + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } + + #[inline] + fn eq(&self, data: &&str) -> bool { + self.as_str() == *data + } +} + +impl From for String { + #[inline] + fn from(name: Name) -> Self { + name.0.into() + } +} + +impl From for CharStr { #[inline] fn from(name: Name) -> Self { name.0 @@ -138,7 +201,7 @@ impl From for compact_str::CompactString { impl salsa::Lookup for Name { #[inline] fn into_owned(self) -> compact_str::CompactString { - self.0 + compact_str::CompactString::new(self.as_str()) } } @@ -146,7 +209,7 @@ impl salsa::Lookup for Name { impl salsa::Lookup for &Name { #[inline] fn into_owned(self) -> compact_str::CompactString { - self.0.clone() + compact_str::CompactString::new(self.as_str()) } } @@ -154,12 +217,12 @@ impl salsa::Lookup for &Name { impl salsa::HashEqLike for compact_str::CompactString { #[inline] fn hash(&self, state: &mut H) { - std::hash::Hash::hash(self, state); + Hash::hash(self, state); } #[inline] fn eq(&self, data: &Name) -> bool { - self == data.as_str() + self.as_str() == data.as_str() } } @@ -167,19 +230,19 @@ impl salsa::HashEqLike for compact_str::CompactString { impl salsa::HashEqLike<&Name> for compact_str::CompactString { #[inline] fn hash(&self, state: &mut H) { - std::hash::Hash::hash(self, state); + Hash::hash(self, state); } #[inline] fn eq(&self, data: &&Name) -> bool { - self == data.as_str() + self.as_str() == data.as_str() } } -impl From for String { +impl From for Name { #[inline] - fn from(name: Name) -> Self { - name.as_str().into() + fn from(name: CharString) -> Self { + Self(name.freeze()) } } @@ -801,8 +864,29 @@ type SegmentsStack<'a> = ArrayVec<&'a str, SMALL_LEN>; #[cfg(test)] mod tests { + #[cfg(feature = "salsa")] + use std::hash::{DefaultHasher, Hash, Hasher}; + + #[cfg(feature = "salsa")] + use crate::name::Name; use crate::name::SegmentsVec; + #[cfg(feature = "salsa")] + #[test] + fn salsa_lookup_name_from_str() { + let name = Name::new("member"); + let lookup = "member"; + + let mut name_hasher = DefaultHasher::new(); + salsa::HashEqLike::<&str>::hash(&name, &mut name_hasher); + let mut lookup_hasher = DefaultHasher::new(); + lookup.hash(&mut lookup_hasher); + + assert_eq!(name_hasher.finish(), lookup_hasher.finish()); + assert!(salsa::HashEqLike::<&str>::eq(&name, &lookup)); + assert_eq!(salsa::Lookup::::into_owned(lookup), name); + } + #[test] fn empty_vec() { let empty = SegmentsVec::new(); diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 927f2c4006..744474099b 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -4078,16 +4078,16 @@ mod tests { #[test] #[cfg(target_pointer_width = "64")] fn size() { - assert_eq!(std::mem::size_of::(), 96); - assert_eq!(std::mem::size_of::(), 96); - assert_eq!(std::mem::size_of::(), 88); + assert_eq!(std::mem::size_of::(), 88); + assert_eq!(std::mem::size_of::(), 88); + assert_eq!(std::mem::size_of::(), 80); assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 80); + assert_eq!(std::mem::size_of::(), 72); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 72); - assert_eq!(std::mem::size_of::(), 64); + assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 24); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); @@ -4105,7 +4105,7 @@ mod tests { assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 48); - assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 12); assert_eq!(std::mem::size_of::(), 40); diff --git a/crates/ruff_python_ast/src/script.rs b/crates/ruff_python_ast/src/script.rs index 287769d338..9180b3ccdb 100644 --- a/crates/ruff_python_ast/src/script.rs +++ b/crates/ruff_python_ast/src/script.rs @@ -20,6 +20,11 @@ pub struct ScriptTag { } impl ScriptTag { + /// Returns the TOML contents of the metadata block. + pub fn metadata(&self) -> &str { + &self.metadata + } + /// Given the contents of a Python file, extract the `script` metadata block with leading /// comment hashes removed, any preceding shebang or content (prelude), and the remaining Python /// script. @@ -52,16 +57,27 @@ impl ScriptTag { // Identify the opening pragma. let index = FINDER.find(contents)?; + Self::parse_at(contents, index) + } + + /// Extracts a `script` metadata block known to start at `index`. + /// + /// Returns `None` if `index` does not point to an exact opening pragma at the start of a line. + pub fn parse_at(contents: &[u8], index: usize) -> Option { + let (prelude, contents) = contents.split_at_checked(index)?; + // The opening pragma must be the first line, or immediately preceded by a newline. - if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) { + if prelude + .last() + .is_some_and(|byte| !matches!(*byte, b'\r' | b'\n')) + { return None; } // Extract the preceding content. - let prelude = std::str::from_utf8(&contents[..index]).ok()?; + let prelude = std::str::from_utf8(prelude).ok()?; // Decode as UTF-8. - let contents = &contents[index..]; let contents = std::str::from_utf8(contents).ok()?; let mut lines = contents.lines(); diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index be037f47a0..14d83eaf54 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index 257d31e608..c998c19eaf 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_codegen). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index b5d48a6aff..c7fe7d8f8d 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 24ceef9085..af1d9cb289 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_formatter). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 42da9edf44..59796785a7 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -95,7 +95,7 @@ where fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>; } -#[derive(Error, Debug, salsa::Update, PartialEq, Eq)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum FormatModuleError { #[error(transparent)] ParseError(#[from] ParseError), diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 08e0178ed5..4bba802a92 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -541,32 +541,26 @@ impl NarrowRange<'_> { // The challenge here is that the second line of the multiline string uses a 4 space indentation. Using `dedent` would // dedent the second line to 0 spaces and the `indent` then adds a 2 space indentation to match the indentation in the source. // This is incorrect because the leading whitespace is the content of the string and not indentation, resulting in changed string content. - if let Some(indentation) = - indentation_at_offset(first_child.start(), self.context.source()) - { - let relative_indent = indentation.strip_prefix(self.enclosing_indent).unwrap(); - let expected_indents = self.level; - - // Each level must always add one level of indent. That's why an empty relative indent to the parent node tells us that the enclosing node is the Module. - let has_expected_indentation = match self.context.options().indent_style() { - IndentStyle::Tab => { - relative_indent.len() == expected_indents - && relative_indent.chars().all(|c| c == '\t') - } - IndentStyle::Space => { - relative_indent.len() - == expected_indents - * self.context.options().indent_width().value() as usize - && relative_indent.chars().all(|c| c == ' ') - } - }; - - if !has_expected_indentation { - return None; + // Missing indentation indicates a simple-statement body of a compound statement (not a suite body). + // Don't narrow the range because the formatter must run `FormatClauseBody` to determine if the body should be collapsed or not. + let indentation = indentation_at_offset(first_child.start(), self.context.source())?; + let relative_indent = indentation.strip_prefix(self.enclosing_indent).unwrap(); + let expected_indents = self.level; + + // Each level must always add one level of indent. That's why an empty relative indent to the parent node tells us that the enclosing node is the Module. + let has_expected_indentation = match self.context.options().indent_style() { + IndentStyle::Tab => { + relative_indent.len() == expected_indents + && relative_indent.chars().all(|c| c == '\t') } - } else { - // Simple-statement body of a compound statement (not a suite body). - // Don't narrow the range because the formatter must run `FormatClauseBody` to determine if the body should be collapsed or not. + IndentStyle::Space => { + relative_indent.len() + == expected_indents * self.context.options().indent_width().value() as usize + && relative_indent.chars().all(|c| c == ' ') + } + }; + + if !has_expected_indentation { return None; } } diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index 7135c376e0..71b81e7e33 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -15,7 +15,7 @@ use ruff_python_parser::ParseOptions; use ruff_python_trivia::TriviaRanges; use { ruff_formatter::{FormatOptions, IndentStyle, LineWidth, Printed, write}, - ruff_python_trivia::{PythonWhitespace, is_python_whitespace}, + ruff_python_trivia::{PythonWhitespace, is_python_whitespace, tab_offset}, ruff_text_size::{Ranged, TextLen, TextRange, TextSize}, }; @@ -1783,7 +1783,7 @@ impl Indentation { for char in iter { if char == '\t' { // Pad to the next multiple of tab_width - width += Self::TAB_INDENT_WIDTH - (width.rem_euclid(Self::TAB_INDENT_WIDTH)); + width += tab_offset(width, Self::TAB_INDENT_WIDTH); len += '\t'.text_len(); } else if char.is_whitespace() { width += char.len_utf8(); @@ -1810,7 +1810,7 @@ impl Indentation { Self::TabSpaces { tabs, spaces } => tabs * Self::TAB_INDENT_WIDTH + spaces, Self::SpacesTabs { spaces, tabs } => { let mut indent = spaces; - indent += Self::TAB_INDENT_WIDTH - indent.rem_euclid(Self::TAB_INDENT_WIDTH); + indent += tab_offset(indent, Self::TAB_INDENT_WIDTH); indent + (tabs - 1) * Self::TAB_INDENT_WIDTH } Self::Mixed { width, .. } => width, @@ -1917,8 +1917,7 @@ impl Indentation { } if char == '\t' { // Pad to the next multiple of tab_width - seen_indent_len += - Self::TAB_INDENT_WIDTH - (seen_indent_len.rem_euclid(Self::TAB_INDENT_WIDTH)); + seen_indent_len += tab_offset(seen_indent_len, Self::TAB_INDENT_WIDTH); trimmed = &trimmed[1..]; } else if char.is_whitespace() { seen_indent_len += char.len_utf8(); diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 09ed138ade..b6d94f4020 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -426,7 +426,7 @@ Formatted once: Formatted twice: --- {reformatted}---"#, - options = &DisplayPyOptions(options), + options = DisplayPyOptions(options), reformatted = reformatted.as_code(), ); } diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 365c950536..2f65a28a62 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 4e2a6a5df8..594ef8b98b 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_importer). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index 12f32a1643..8278131bbd 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index 837ade593e..0a11540f61 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_index). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index e19602e4dd..1fd8bebb7f 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index c8afd008f7..afdcd0368b 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_literal). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index f1eba8e8f5..37653ab1de 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -5,12 +5,6 @@ use std::str::FromStr; use crate::Case; -trait FormatParse { - fn parse(text: &str) -> (Option, &str) - where - Self: Sized; -} - #[derive(Debug, Copy, Clone, PartialEq)] pub enum FormatConversion { Str, @@ -19,7 +13,7 @@ pub enum FormatConversion { Bytes, } -impl FormatParse for FormatConversion { +impl FormatConversion { fn parse(text: &str) -> (Option, &str) { let Some(conversion) = Self::from_string(text) else { return (None, text); @@ -72,7 +66,7 @@ impl FormatAlign { } } -impl FormatParse for FormatAlign { +impl FormatAlign { fn parse(text: &str) -> (Option, &str) { let mut chars = text.chars(); if let Some(maybe_align) = chars.next().and_then(Self::from_char) { @@ -90,15 +84,16 @@ pub enum FormatSign { MinusOrSpace, } -impl FormatParse for FormatSign { +impl FormatSign { fn parse(text: &str) -> (Option, &str) { let mut chars = text.chars(); - match chars.next() { - Some('-') => (Some(Self::Minus), chars.as_str()), - Some('+') => (Some(Self::Plus), chars.as_str()), - Some(' ') => (Some(Self::MinusOrSpace), chars.as_str()), - _ => (None, text), - } + let kind = match chars.next() { + Some('-') => Self::Minus, + Some('+') => Self::Plus, + Some(' ') => Self::MinusOrSpace, + Some(_) | None => return (None, text), + }; + (Some(kind), chars.as_str()) } } @@ -108,13 +103,13 @@ pub enum FormatGrouping { Underscore, } -impl FormatParse for FormatGrouping { +impl FormatGrouping { fn parse(text: &str) -> (Option, &str) { let mut chars = text.chars(); match chars.next() { Some('_') => (Some(Self::Underscore), chars.as_str()), Some(',') => (Some(Self::Comma), chars.as_str()), - _ => (None, text), + Some(_) | None => (None, text), } } } @@ -157,29 +152,40 @@ impl From<&FormatType> for char { } } -impl FormatParse for FormatType { - fn parse(text: &str) -> (Option, &str) { +impl FormatType { + /// Attempt to parse the [conversion type] of the f-string. + /// + /// A conversion type is optional in an f-string. + /// If it is present, it is always the last character of the format specifier. + /// + /// If a valid conversion type was encountered, this function returns `Ok((Some(FormatType), remaining_text))`. + /// If an invalid conversion type was encountered, this function returns `Err(invalid_char)`. + /// If no conversion type was encountered, this function returns `Ok((None, remaining_text))`. + /// + /// [conversion type]: https://docs.python.org/3/library/string.html#format-specification-mini-language + fn parse(text: &str) -> Result<(Option, &str), char> { let mut chars = text.chars(); - match chars.next() { - Some('s') => (Some(Self::String), chars.as_str()), - Some('b') => (Some(Self::Binary), chars.as_str()), - Some('c') => (Some(Self::Character), chars.as_str()), - Some('d') => (Some(Self::Decimal), chars.as_str()), - Some('o') => (Some(Self::Octal), chars.as_str()), - Some('n') => (Some(Self::Number(Case::Lower)), chars.as_str()), - Some('N') => (Some(Self::Number(Case::Upper)), chars.as_str()), - Some('x') => (Some(Self::Hex(Case::Lower)), chars.as_str()), - Some('X') => (Some(Self::Hex(Case::Upper)), chars.as_str()), - Some('e') => (Some(Self::Exponent(Case::Lower)), chars.as_str()), - Some('E') => (Some(Self::Exponent(Case::Upper)), chars.as_str()), - Some('f') => (Some(Self::FixedPoint(Case::Lower)), chars.as_str()), - Some('F') => (Some(Self::FixedPoint(Case::Upper)), chars.as_str()), - Some('g') => (Some(Self::GeneralFormat(Case::Lower)), chars.as_str()), - Some('G') => (Some(Self::GeneralFormat(Case::Upper)), chars.as_str()), - Some('%') => (Some(Self::Percentage), chars.as_str()), - Some(_) => (None, chars.as_str()), - _ => (None, text), - } + let kind = match chars.next() { + Some('s') => Self::String, + Some('b') => Self::Binary, + Some('c') => Self::Character, + Some('d') => Self::Decimal, + Some('o') => Self::Octal, + Some('n') => Self::Number(Case::Lower), + Some('N') => Self::Number(Case::Upper), + Some('x') => Self::Hex(Case::Lower), + Some('X') => Self::Hex(Case::Upper), + Some('e') => Self::Exponent(Case::Lower), + Some('E') => Self::Exponent(Case::Upper), + Some('f') => Self::FixedPoint(Case::Lower), + Some('F') => Self::FixedPoint(Case::Upper), + Some('g') => Self::GeneralFormat(Case::Lower), + Some('G') => Self::GeneralFormat(Case::Upper), + Some('%') => Self::Percentage, + Some(invalid) => return Err(invalid), + None => return Ok((None, text)), + }; + Ok((Some(kind), chars.as_str())) } } @@ -297,7 +303,7 @@ fn parse_alternate_form(text: &str) -> (bool, &str) { let mut chars = text.chars(); match chars.next() { Some('#') => (true, chars.as_str()), - _ => (false, text), + Some(_) | None => (false, text), } } @@ -323,7 +329,7 @@ fn parse_precision(text: &str) -> Result<(Option, &str), FormatSpecError> (None, text) } } - _ => (None, text), + Some(_) | None => (None, text), }) } @@ -368,20 +374,13 @@ impl FormatSpec { let (grouping_option, text) = FormatGrouping::parse(text); let (precision, text) = parse_precision(text)?; - let (format_type, _text) = if text.is_empty() { - (None, text) - } else { - // If there's any remaining text, we should yield a valid format type and consume it - // all. - let (format_type, text) = FormatType::parse(text); - if format_type.is_none() { - return Err(FormatSpecError::InvalidFormatType); - } - if !text.is_empty() { - return Err(FormatSpecError::InvalidFormatSpecifier); - } - (format_type, text) - }; + // If there's any remaining text, we should yield a valid format type and consume it + // all. + let (format_type, text) = + FormatType::parse(text).map_err(FormatSpecError::InvalidFormatType)?; + if !text.is_empty() { + return Err(FormatSpecError::InvalidFormatSpecifier); + } if zero && fill.is_none() { fill.replace('0'); @@ -407,7 +406,7 @@ pub enum FormatSpecError { DecimalDigitsTooMany, PrecisionTooBig, InvalidFormatSpecifier, - InvalidFormatType, + InvalidFormatType(char), InvalidPlaceholder(FormatParseError), PlaceholderRecursionExceeded, UnspecifiedFormat(char, char), @@ -969,7 +968,7 @@ mod tests { ); assert_eq!( FormatSpec::parse("}"), - Err(FormatSpecError::InvalidFormatType) + Err(FormatSpecError::InvalidFormatType('}')) ); assert_eq!( FormatSpec::parse("{}}"), @@ -995,7 +994,7 @@ mod tests { ); assert_eq!( FormatSpec::parse("z"), - Err(FormatSpecError::InvalidFormatType) + Err(FormatSpecError::InvalidFormatType('z')) ); } diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index 8de52b7625..aebe2f1ae3 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } @@ -19,7 +19,7 @@ ruff_text_size = { workspace = true, features = ["get-size"] } bitflags = { workspace = true } bstr = { workspace = true } -compact_str = { workspace = true } +drop_bomb = { workspace = true } get-size2 = { workspace = true } memchr = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index 115815a968..e8a07ae1e2 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_parser). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 93208aebc8..062b7898f6 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -30,7 +30,7 @@ impl std::error::Error for ParseError { impl fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{} at byte range {:?}", &self.error, self.location) + write!(f, "{} at byte range {:?}", self.error, self.location) } } diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 89426fb20a..ad4d984360 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -144,7 +144,7 @@ impl<'src> Lexer<'src> { /// Lex the next token. pub fn next_token(&mut self) -> TokenKind { - self.cursor.start_token(); + // `lex_token` marks the start on the path that lexes each token. self.current_flags = TokenFlags::empty(); self.current_kind = self.lex_token(); // For `Unknown` token, the `push_error` method updates the current range. @@ -157,6 +157,7 @@ impl<'src> Lexer<'src> { fn lex_token(&mut self) -> TokenKind { if let Some(interpolated_string) = self.interpolated_strings.current() { if !interpolated_string.is_in_interpolation(self.nesting) { + self.cursor.start_token(); if let Some(token) = self.lex_interpolated_string_middle_or_end() { if token.is_interpolated_string_end() { self.interpolated_strings.pop(); @@ -166,7 +167,11 @@ impl<'src> Lexer<'src> { } } // Return dedent tokens until the current indentation level matches the indentation of the next token. - else if let Some(indentation) = self.pending_indentation.take() { + // Avoid `Option::take` here: this check runs for every token, and `take` writes `None` + // even when there is no pending indentation. + else if let Some(indentation) = self.pending_indentation { + self.pending_indentation = None; + self.cursor.start_token(); match self.indentations.current().try_compare(indentation) { Ok(Ordering::Greater) => { self.pending_indentation = Some(indentation); @@ -189,6 +194,8 @@ impl<'src> Lexer<'src> { } if self.state.is_after_newline() { + // Indent and dedent tokens include leading whitespace in their ranges. + self.cursor.start_token(); if let Some(indentation) = self.eat_indentation() { return indentation; } @@ -198,7 +205,7 @@ impl<'src> Lexer<'src> { } } - // The lexer might've skipped whitespaces, so update the start offset + // Whitespace between tokens is not part of the next token's range. self.cursor.start_token(); if let Some(c) = self.cursor.bump() { @@ -347,6 +354,12 @@ impl<'src> Lexer<'src> { } fn skip_whitespace(&mut self) -> Result<(), LexicalError> { + let whitespace_start = if matches!(self.cursor.first(), ' ' | '\t' | '\\' | '\x0C') { + self.offset() + } else { + return Ok(()); + }; + loop { match self.cursor.first() { ' ' => { @@ -366,7 +379,10 @@ impl<'src> Lexer<'src> { )); } if self.cursor.is_eof() { - return Err(LexicalError::new(LexicalErrorType::Eof, self.token_range())); + return Err(LexicalError::new( + LexicalErrorType::Eof, + TextRange::new(whitespace_start, self.offset()), + )); } } // Form feed @@ -673,19 +689,27 @@ impl<'src> Lexer<'src> { /// Lex an identifier. Also used for keywords and string/bytes literals with a prefix. fn lex_identifier(&mut self, first: char) -> TokenKind { // Detect potential string like rb'' b'' f'' t'' u'' r'' - let quote = match (first, self.cursor.first()) { - (_, quote @ ('\'' | '"')) => self.try_single_char_prefix(first).then(|| { - self.cursor.bump(); - quote - }), - (_, second) if is_quote(self.cursor.second()) => { - self.try_double_char_prefix([first, second]).then(|| { + let quote = if let Some(prefix) = single_char_prefix(first) { + match self.cursor.first() { + quote @ ('\'' | '"') => { + self.current_flags |= prefix; self.cursor.bump(); - // SAFETY: Safe because of the `is_quote` check in this match arm's guard - self.cursor.bump().unwrap() - }) + Some(quote) + } + second + if let quote = self.cursor.second() + && is_quote(quote) => + { + self.try_double_char_prefix([first, second]).then(|| { + self.cursor.bump(); + self.cursor.bump(); + quote + }) + } + _ => None, } - _ => None, + } else { + None }; if let Some(quote) = quote { @@ -767,21 +791,6 @@ impl<'src> Lexer<'src> { } } - /// Try lexing the single character string prefix, updating the token flags accordingly. - /// Returns `true` if it matches. - fn try_single_char_prefix(&mut self, first: char) -> bool { - match first { - 'f' | 'F' => self.current_flags |= TokenFlags::F_STRING, - 't' | 'T' => self.current_flags |= TokenFlags::T_STRING, - 'u' | 'U' => self.current_flags |= TokenFlags::UNICODE_STRING, - 'b' | 'B' => self.current_flags |= TokenFlags::BYTE_STRING, - 'r' => self.current_flags |= TokenFlags::RAW_STRING_LOWERCASE, - 'R' => self.current_flags |= TokenFlags::RAW_STRING_UPPERCASE, - _ => return false, - } - true - } - /// Try lexing the double character string prefix, updating the token flags accordingly. /// Returns `true` if it matches. fn try_double_char_prefix(&mut self, value: [char; 2]) -> bool { @@ -1619,6 +1628,18 @@ const fn is_quote(c: char) -> bool { matches!(c, '\'' | '"') } +fn single_char_prefix(c: char) -> Option { + Some(match c { + 'f' | 'F' => TokenFlags::F_STRING, + 't' | 'T' => TokenFlags::T_STRING, + 'u' | 'U' => TokenFlags::UNICODE_STRING, + 'b' | 'B' => TokenFlags::BYTE_STRING, + 'r' => TokenFlags::RAW_STRING_LOWERCASE, + 'R' => TokenFlags::RAW_STRING_UPPERCASE, + _ => return None, + }) +} + const fn is_ascii_identifier_start(c: char) -> bool { matches!(c, 'a'..='z' | 'A'..='Z' | '_') } diff --git a/crates/ruff_python_parser/src/lexer/indentation.rs b/crates/ruff_python_parser/src/lexer/indentation.rs index 7125f3a224..c2193c9e7b 100644 --- a/crates/ruff_python_parser/src/lexer/indentation.rs +++ b/crates/ruff_python_parser/src/lexer/indentation.rs @@ -2,6 +2,8 @@ use static_assertions::assert_eq_size; use std::cmp::Ordering; use std::fmt::Debug; +use ruff_python_trivia::tab_offset_u32; + /// The column index of an indentation. /// /// A space increments the column by one. A tab adds up to 2 (if tab size is 2) indices, but just one @@ -63,7 +65,7 @@ impl Indentation { // * Adds `TAB_SIZE` if `column` is a multiple of `TAB_SIZE` // * Rounds `column` up to the next multiple of `TAB_SIZE` otherwise. // https://github.com/python/cpython/blob/2cf99026d6320f38937257da1ab014fc873a11a6/Parser/tokenizer.c#L1818 - column: Column((self.column.0 / Self::TAB_SIZE + 1) * Self::TAB_SIZE), + column: Column(self.column.0 + tab_offset_u32(self.column.0, Self::TAB_SIZE)), } } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index d22750bd76..053186dfc6 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -900,7 +900,8 @@ impl<'src> Parser<'src> { } if self.current_token_kind().is_soft_keyword() { - let id = Name::new(self.src_text(range)); + let text = self.src_text(range); + let id = self.intern_name(text); self.bump_soft_keyword_as_name(); return ast::Identifier { id, @@ -934,7 +935,8 @@ impl<'src> Parser<'src> { range, ); - let id = Name::new(self.src_text(range)); + let text = self.src_text(range); + let id = self.intern_name(text); self.bump_any(); ast::Identifier { id, @@ -1320,8 +1322,8 @@ impl<'src> Parser<'src> { }; } - let mut args = vec![]; - let mut keywords = vec![]; + let args_snapshot = self.expr_scratch.snapshot(); + let keywords_snapshot = self.keyword_scratch.snapshot(); let mut seen_keyword_argument = false; // foo = 1 let mut seen_keyword_unpacking = false; // **foo @@ -1331,7 +1333,7 @@ impl<'src> Parser<'src> { if parser.eat(TokenKind::DoubleStar) { let value = parser.parse_conditional_expression_or_higher(); - keywords.push(ast::Keyword { + parser.keyword_scratch.push(ast::Keyword { arg: None, value: value.expr, range: parser.node_range(argument_start), @@ -1421,7 +1423,7 @@ impl<'src> Parser<'src> { let value = parser.parse_conditional_expression_or_higher(); - keywords.push(ast::Keyword { + parser.keyword_scratch.push(ast::Keyword { arg: Some(arg), value: value.expr, range: parser.node_range(argument_start), @@ -1441,23 +1443,19 @@ impl<'src> Parser<'src> { ); } } - // Reserve exactly one slot for the first positional argument, while - // avoiding any allocation for keyword-only calls. - if args.is_empty() { - args.reserve_exact(1); - } - args.push(parsed_expr.expr); + parser.expr_scratch.push(parsed_expr.expr); } } }); self.expect(TokenKind::Rpar); + let keywords = self.keyword_scratch.take_thin_vec(keywords_snapshot); let arguments = ast::Arguments { range: self.node_range(start), node_index: AtomicNodeIndex::NONE, - args: args.into_boxed_slice(), - keywords: keywords.into(), + args: self.expr_scratch.take(args_snapshot), + keywords, }; self.validate_arguments(&arguments, has_trailing_comma, context); @@ -1586,7 +1584,8 @@ impl<'src> Parser<'src> { // If there are more than one element in the slice, we need to create a tuple // expression to represent it. if self.eat(TokenKind::Comma) { - let mut slices = vec![slice]; + let slices_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(slice); self.parse_comma_separated_list(RecoveryContextKind::Slices, |parser| { // basedpython: bare `*` element (top-star marker) terminated @@ -1606,7 +1605,7 @@ impl<'src> Parser<'src> { ctx: ExprContext::Invalid, node_index: AtomicNodeIndex::NONE, }); - slices.push(Expr::Starred(ast::ExprStarred { + parser.expr_scratch.push(Expr::Starred(ast::ExprStarred { value: Box::new(marker_name), ctx: ExprContext::Load, range: star_range, @@ -1638,7 +1637,7 @@ impl<'src> Parser<'src> { let inner = parser .parse_conditional_expression_or_higher_impl(ExpressionContext::default()); let field_range = parser.node_range(field_start); - slices.push(Expr::Named(ast::ExprNamed { + parser.expr_scratch.push(Expr::Named(ast::ExprNamed { target: Box::new(Expr::Name(target_name)), value: Box::new(inner.expr), range: field_range, @@ -1650,13 +1649,11 @@ impl<'src> Parser<'src> { if let Some(variance) = variance_marker { element = Self::wrap_variance_marker(element, variance, variance_marker_range); } - slices.push(element); + parser.expr_scratch.push(element); }); - slices.shrink_to_fit(); - slice = Expr::Tuple(ast::ExprTuple { - elts: slices, + elts: self.expr_scratch.take(slices_snapshot), ctx: ExprContext::Load, range: self.node_range(slice_start), parenthesized: false, @@ -1933,8 +1930,8 @@ impl<'src> Parser<'src> { ) -> ast::ExprBoolOp { self.bump(TokenKind::from(op)); - let mut values = Vec::with_capacity(2); - values.push(lhs); + let values_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(lhs); let mut progress = ParserProgress::default(); // Keep adding the expression to `values` until we see a different @@ -1944,17 +1941,15 @@ impl<'src> Parser<'src> { let parsed_expr = self.parse_binary_expression_or_higher(OperatorPrecedence::from(op), context); - values.push(parsed_expr.expr); + self.expr_scratch.push(parsed_expr.expr); if !self.eat(TokenKind::from(op)) { break; } } - values.shrink_to_fit(); - ast::ExprBoolOp { - values, + values: self.expr_scratch.take(values_snapshot), op, range: self.node_range(start), node_index: AtomicNodeIndex::NONE, @@ -2017,7 +2012,7 @@ impl<'src> Parser<'src> { ) -> ast::ExprCompare { self.bump_cmp_op(op); - let mut comparators = vec![]; + let comparators_snapshot = self.expr_scratch.snapshot(); let mut operators = vec![op]; let mut progress = ParserProgress::default(); @@ -2025,13 +2020,13 @@ impl<'src> Parser<'src> { loop { progress.assert_progressing(self); - comparators.push( - self.parse_binary_expression_or_higher( + let comparator = self + .parse_binary_expression_or_higher( OperatorPrecedence::ComparisonsMembershipIdentity, context, ) - .expr, - ); + .expr; + self.expr_scratch.push(comparator); let next_token = self.current_token_kind(); if matches!(next_token, TokenKind::In) && context.is_in_excluded() { @@ -2051,7 +2046,7 @@ impl<'src> Parser<'src> { ast::ExprCompare { left: Box::new(lhs), ops: operators.into_boxed_slice(), - comparators: comparators.into_boxed_slice(), + comparators: self.expr_scratch.take(comparators_snapshot), range: self.node_range(start), node_index: AtomicNodeIndex::NONE, } @@ -3315,20 +3310,20 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Comma); } - let mut elts = vec![first_element]; + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element); self.parse_comma_separated_list(RecoveryContextKind::TupleElements(parenthesized), |p| { - elts.push(parse_func(p).expr); + let element = parse_func(p).expr; + p.expr_scratch.push(element); }); if parenthesized.is_yes() { self.expect(TokenKind::Rpar); } - elts.shrink_to_fit(); - ast::ExprTuple { - elts, + elts: self.expr_scratch.take(elts_snapshot), ctx: ExprContext::Load, range: self.node_range(start), node_index: AtomicNodeIndex::NONE, @@ -3897,22 +3892,20 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Comma); } - let mut elts = vec![first_element]; + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element); self.parse_comma_separated_list(RecoveryContextKind::ListElements, |parser| { - elts.push( - parser - .parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) - .expr, - ); + let element = parser + .parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + .expr; + parser.expr_scratch.push(element); }); self.expect(TokenKind::Rsqb); - elts.shrink_to_fit(); - ast::ExprList { - elts, + elts: self.expr_scratch.take(elts_snapshot), ctx: ExprContext::Load, range: self.node_range(start), node_index: AtomicNodeIndex::NONE, @@ -3942,7 +3935,8 @@ impl<'src> Parser<'src> { ); } - let mut elts = vec![first_element.expr]; + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element.expr); self.parse_comma_separated_list(RecoveryContextKind::SetElements, |parser| { let parsed_expr = @@ -3957,7 +3951,7 @@ impl<'src> Parser<'src> { ); } - elts.push(parsed_expr.expr); + parser.expr_scratch.push(parsed_expr.expr); }); self.expect(TokenKind::Rbrace); @@ -3965,7 +3959,7 @@ impl<'src> Parser<'src> { ast::ExprSet { range: self.node_range(start), node_index: AtomicNodeIndex::NONE, - elts, + elts: self.expr_scratch.take(elts_snapshot), } } @@ -4094,7 +4088,7 @@ impl<'src> Parser<'src> { self.expect(TokenKind::In); let iter = self.parse_simple_expression(ExpressionContext::default()); - let mut ifs = Vec::new(); + let ifs_snapshot = self.expr_scratch.snapshot(); let mut progress = ParserProgress::default(); while self.eat(TokenKind::If) { @@ -4102,20 +4096,15 @@ impl<'src> Parser<'src> { let parsed_expr = self.parse_simple_expression(ExpressionContext::default()); - if ifs.is_empty() { - ifs.reserve_exact(1); - } - ifs.push(parsed_expr.expr); + self.expr_scratch.push(parsed_expr.expr); } - ifs.shrink_to_fit(); - ast::Comprehension { range: self.node_range(start), node_index: AtomicNodeIndex::NONE, target: target.expr, iter: iter.expr, - ifs, + ifs: self.expr_scratch.take(ifs_snapshot), is_async, } } diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index d2762e8d28..1750829d5e 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -6,16 +6,19 @@ use bitflags::bitflags; use ruff_python_ast::name::Name; use ruff_python_ast::token::TokenKind; use ruff_python_ast::{ - AtomicNodeIndex, Int, IpyEscapeKind, Mod, ModExpression, ModModule, StringFlags, + Alias, AtomicNodeIndex, ElifElseClause, Expr, Int, IpyEscapeKind, Keyword, Mod, ModExpression, + ModModule, ParameterWithDefault, Stmt, StringFlags, }; use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{Ranged, TextRange, TextSize}; +use rustc_hash::FxHashSet; use thin_vec::ThinVec; use unicode_normalization::UnicodeNormalization; use crate::error::UnsupportedSyntaxError; use crate::parser::expression::ExpressionContext; use crate::parser::progress::{ParserProgress, TokenId}; +use crate::parser::scratch_buffer::ScratchBuffer; use crate::string::InterpolatedStringKind; use crate::token_set::TokenSet; use crate::token_source::{TokenSource, TokenSourceCheckpoint}; @@ -30,10 +33,33 @@ mod options; mod pattern; mod progress; mod recovery; +mod scratch_buffer; mod statement; #[cfg(test)] mod tests; +#[derive(Debug, Default)] +struct NameInterner { + names: FxHashSet, +} + +impl NameInterner { + /// Returns an inline name directly, or a shared clone of a heap-allocated name. + fn intern(&mut self, text: &str) -> Name { + if let Some(name) = Name::new_inline(text) { + return name; + } + + if let Some(name) = self.names.get(text) { + return name.clone(); + } + + let name = Name::new_heap(text); + self.names.insert(name.clone()); + name + } +} + #[derive(Debug)] pub(crate) struct Parser<'src> { source: &'src str, @@ -41,6 +67,12 @@ pub(crate) struct Parser<'src> { /// Token source for the parser that skips over any non-trivia token. tokens: TokenSource<'src>, + /// Deduplicates the backing allocations for repeated names that do not fit inline. + name_interner: NameInterner, + + /// Reusable storage for names that need to be constructed by the parser. + name_buffer: String, + /// Stores all the syntax errors found during the parsing. errors: Vec, @@ -72,6 +104,24 @@ pub(crate) struct Parser<'src> { /// Maximum lexer nesting depth before postfix calls and subscripts should stop recursing. max_nesting_depth: u32, + + /// Reusable, nesting-safe scratch storage for expression lists. + expr_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for call keywords. + keyword_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for function and lambda parameters. + parameter_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for statement lists. + stmt_scratch: ScratchBuffer, + + /// Reusable scratch storage for import aliases. + alias_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for `elif` and `else` clauses. + elif_else_scratch: ScratchBuffer, } impl<'src> Parser<'src> { @@ -96,6 +146,8 @@ impl<'src> Parser<'src> { errors: Vec::new(), unsupported_syntax_errors: Vec::new(), tokens, + name_interner: NameInterner::default(), + name_buffer: String::new(), recovery_context: RecoveryContext::empty(), prev_token_end: TextSize::new(0), start_offset, @@ -103,6 +155,12 @@ impl<'src> Parser<'src> { class_body_depth: 0, depth_remaining, max_nesting_depth, + expr_scratch: ScratchBuffer::with_capacity(16), + keyword_scratch: ScratchBuffer::new(), + parameter_scratch: ScratchBuffer::new(), + stmt_scratch: ScratchBuffer::with_capacity(32), + alias_scratch: ScratchBuffer::new(), + elif_else_scratch: ScratchBuffer::new(), } } @@ -212,7 +270,6 @@ impl<'src> Parser<'src> { TokenKind::EndOfFile, "Parser should be at the end of the file." ); - // TODO consider re-integrating lexical error handling into the parser? let parse_errors = self.errors; let (tokens, lex_errors) = self.tokens.finish(); @@ -409,14 +466,28 @@ impl<'src> Parser<'src> { fn bump_name(&mut self) -> Name { let text = self.current_token_text(); let name = if !self.tokens.current_flags().is_non_ascii_name() { - Name::new(text) + self.intern_name(text) } else { - normalize_name(text) + self.intern_normalized_name(text) }; self.bump(TokenKind::Name); name } + fn intern_name(&mut self, text: &str) -> Name { + self.name_interner.intern(text) + } + + fn intern_normalized_name(&mut self, text: &str) -> Name { + let snapshot = self.name_buffer.len(); + self.name_buffer.extend(text.nfkc()); + + let name = self.name_interner.intern(&self.name_buffer[snapshot..]); + + self.name_buffer.truncate(snapshot); + name + } + fn bump_int(&mut self) -> Int { let text = self.current_token_text(); let value = if let Some(digits) = @@ -923,11 +994,6 @@ fn strip_underscores(text: &str) -> Cow<'_, str> { } } -#[cold] -fn normalize_name(text: &str) -> Name { - text.nfkc().collect::() -} - #[derive(Copy, Clone)] enum IpyEscapeContext { Assignment, diff --git a/crates/ruff_python_parser/src/parser/scratch_buffer.rs b/crates/ruff_python_parser/src/parser/scratch_buffer.rs new file mode 100644 index 0000000000..9f3acb015e --- /dev/null +++ b/crates/ruff_python_parser/src/parser/scratch_buffer.rs @@ -0,0 +1,136 @@ +use std::vec::Drain; + +use drop_bomb::DebugDropBomb; +use thin_vec::ThinVec; + +/// Reusable scratch storage that preserves entries belonging to outer parser frames. +#[derive(Debug)] +pub(super) struct ScratchBuffer { + buffer: Vec, +} + +impl ScratchBuffer { + pub(super) fn new() -> Self { + Self { buffer: Vec::new() } + } + + pub(super) fn with_capacity(capacity: usize) -> Self { + Self { + buffer: Vec::with_capacity(capacity), + } + } + + #[inline] + pub(super) fn push(&mut self, value: T) { + self.buffer.push(value); + } + + #[inline] + pub(super) fn is_empty(&self, snapshot: &ScratchSnapshot) -> bool { + debug_assert!( + self.buffer.len() >= snapshot.len, + "Scratch buffer snapshots must be restored in reverse order of creation." + ); + self.buffer.len() == snapshot.len + } + + #[inline] + pub(super) fn snapshot(&self) -> ScratchSnapshot { + ScratchSnapshot { + len: self.buffer.len(), + bomb: DebugDropBomb::new("Scratch buffer snapshots must be restored."), + } + } + + #[inline] + pub(super) fn take>(&mut self, snapshot: ScratchSnapshot) -> C { + self.drain_snapshot(snapshot).collect() + } + + #[inline] + pub(super) fn take_thin_vec(&mut self, mut snapshot: ScratchSnapshot) -> ThinVec { + if self.is_empty(&snapshot) { + snapshot.bomb.defuse(); + return ThinVec::new(); + } + + let drain = self.drain_snapshot(snapshot); + let mut result = ThinVec::with_capacity(drain.len()); + result.extend(drain); + result + } + + #[inline] + fn drain_snapshot(&mut self, mut snapshot: ScratchSnapshot) -> Drain<'_, T> { + debug_assert!( + self.buffer.len() >= snapshot.len, + "Scratch buffer snapshots must be restored in reverse order of creation." + ); + snapshot.bomb.defuse(); + self.buffer.drain(snapshot.len..) + } +} + +impl Drop for ScratchBuffer { + fn drop(&mut self) { + debug_assert!( + self.buffer.is_empty() || std::thread::panicking(), + "Scratch buffers must be empty when dropped." + ); + } +} + +pub(super) struct ScratchSnapshot { + len: usize, + bomb: DebugDropBomb, +} + +#[cfg(test)] +mod tests { + use super::ScratchBuffer; + + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "Scratch buffers must be empty when dropped.")] + fn buffer_must_be_empty_when_dropped() { + let mut buffer = ScratchBuffer::new(); + buffer.push(1); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "Scratch buffer snapshots must be restored.")] + fn snapshot_must_be_restored() { + let buffer = ScratchBuffer::::new(); + let _snapshot = buffer.snapshot(); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic( + expected = "Scratch buffer snapshots must be restored in reverse order of creation." + )] + fn snapshot_must_be_restored_in_reverse_order() { + let mut buffer = ScratchBuffer::new(); + buffer.push(1); + let snapshot = buffer.snapshot(); + buffer.buffer.clear(); + + let _: Vec<_> = buffer.take(snapshot); + } + + #[test] + fn snapshot_is_empty_relative_to_its_buffer() { + let mut buffer = ScratchBuffer::new(); + let outer_snapshot = buffer.snapshot(); + buffer.push(1); + let snapshot = buffer.snapshot(); + assert!(buffer.is_empty(&snapshot)); + + buffer.push(2); + assert!(!buffer.is_empty(&snapshot)); + + let _: Vec<_> = buffer.take(snapshot); + let _: Vec<_> = buffer.take(outer_snapshot); + } +} diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 8a7627aa7e..28396f81c5 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1,4 +1,3 @@ -use compact_str::CompactString; use std::fmt::{Display, Write}; use ruff_python_ast::name::Name; @@ -1658,20 +1657,21 @@ impl<'src> Parser<'src> { /// /// [Python grammar]: https://docs.python.org/3/reference/grammar.html fn parse_simple_statements(&mut self) -> Suite { - let mut stmts = Suite::with_capacity(1); + let stmts_snapshot = self.stmt_scratch.snapshot(); let mut progress = ParserProgress::default(); loop { progress.assert_progressing(self); - stmts.push(self.parse_simple_statement()); + let stmt = self.parse_simple_statement(); + let is_trailing_lambda = + matches!(&stmt, Stmt::FunctionDef(function) if function.is_trailing_lambda); + self.stmt_scratch.push(stmt); // basedpython: a trailing lambda block consumed its own suite — // no semicolon or newline follows it - if matches!(stmts.last(), Some(Stmt::FunctionDef(function)) if function.is_trailing_lambda) - { - stmts.shrink_to_fit(); - return stmts; + if is_trailing_lambda { + return self.stmt_scratch.take_thin_vec(stmts_snapshot); } if !self.eat(TokenKind::Semi) { @@ -1729,8 +1729,7 @@ impl<'src> Parser<'src> { // test_ok simple_stmts_with_semicolons // return; import a; from x import y; z; type T = int - stmts.shrink_to_fit(); - stmts + self.stmt_scratch.take_thin_vec(stmts_snapshot) } /// Parses a simple statement. @@ -2117,11 +2116,12 @@ impl<'src> Parser<'src> { // import , // import x, y, - let mut names = self.parse_comma_separated_list_into_vec_with_capacity( - RecoveryContextKind::ImportNames, - |p| p.parse_alias(ImportStyle::Import), - 1, - ); + let names_snapshot = self.alias_scratch.snapshot(); + self.parse_comma_separated_list(RecoveryContextKind::ImportNames, |parser| { + let alias = parser.parse_alias(ImportStyle::Import); + parser.alias_scratch.push(alias); + }); + let names: Vec<_> = self.alias_scratch.take(names_snapshot); if names.is_empty() { // test_err import_stmt_empty @@ -2129,8 +2129,6 @@ impl<'src> Parser<'src> { self.add_error(ParseErrorType::EmptyImportNames, self.current_token_range()); } - names.shrink_to_fit(); - ast::StmtImport { names, is_lazy, @@ -2195,7 +2193,7 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Import); let names_start = self.node_start(); - let mut names = Vec::new(); + let names_snapshot = self.alias_scratch.snapshot(); let mut seen_star_import = false; let parenthesized = Parenthesized::from(self.eat(TokenKind::Lpar)); @@ -2213,9 +2211,10 @@ impl<'src> Parser<'src> { // from x import a, b.c, d, e.f, g let alias = parser.parse_alias(ImportStyle::ImportFrom); seen_star_import |= alias.name.id == "*"; - names.push(alias); + parser.alias_scratch.push(alias); }, ); + let names: Vec<_> = self.alias_scratch.take(names_snapshot); if names.is_empty() { // test_err from_import_empty_names @@ -2255,8 +2254,6 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Rpar); } - names.shrink_to_fit(); - ast::StmtImportFrom { module, names, @@ -2328,7 +2325,13 @@ impl<'src> Parser<'src> { fn parse_dotted_name(&mut self) -> ast::Identifier { let start = self.node_start(); - let mut dotted_name: CompactString = self.parse_identifier().id.into(); + let first = self.parse_identifier(); + if !self.at(TokenKind::Dot) { + return first; + } + + let snapshot = self.name_buffer.len(); + self.name_buffer.push_str(&first.id); let mut progress = ParserProgress::default(); while self.eat(TokenKind::Dot) { @@ -2337,15 +2340,19 @@ impl<'src> Parser<'src> { // test_err dotted_name_multiple_dots // import a..b // import a...b - dotted_name.push('.'); - dotted_name.push_str(&self.parse_identifier()); + self.name_buffer.push('.'); + let identifier = self.parse_identifier(); + self.name_buffer.push_str(&identifier.id); } + let id = self.name_interner.intern(&self.name_buffer[snapshot..]); + self.name_buffer.truncate(snapshot); + // test_ok dotted_name_normalized_spaces // import a.b.c // import a . b . c ast::Identifier { - id: Name::from(dotted_name), + id, range: self.node_range(start), node_index: AtomicNodeIndex::NONE, } @@ -2950,23 +2957,21 @@ impl<'src> Parser<'src> { // pass // else: // pass - let mut elif_else_clauses = self.parse_clauses(Clause::ElIf, |p| { - p.parse_elif_or_else_clause(ElifOrElse::Elif) + let elif_else_snapshot = self.elif_else_scratch.snapshot(); + self.parse_clauses(Clause::ElIf, |parser| { + let clause = parser.parse_elif_or_else_clause(ElifOrElse::Elif); + parser.elif_else_scratch.push(clause); }); if self.at(TokenKind::Else) { - if elif_else_clauses.is_empty() { - elif_else_clauses.reserve_exact(1); - } - elif_else_clauses.push(self.parse_elif_or_else_clause(ElifOrElse::Else)); + let clause = self.parse_elif_or_else_clause(ElifOrElse::Else); + self.elif_else_scratch.push(clause); } - elif_else_clauses.shrink_to_fit(); - ast::StmtIf { test: Box::new(test.expr), body, - elif_else_clauses, + elif_else_clauses: self.elif_else_scratch.take(elif_else_snapshot), range: self.node_range(start), node_index: AtomicNodeIndex::NONE, } @@ -3058,7 +3063,8 @@ impl<'src> Parser<'src> { // except* ExceptionGroup: // pass let mut mixed_except_ranges = Vec::new(); - let mut handlers = self.parse_clauses(Clause::Except, |p| { + let mut handlers = Vec::new(); + self.parse_clauses(Clause::Except, |p| { let (handler, kind) = p.parse_except_clause(); if let ExceptClauseKind::Star(range) = kind { p.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::ExceptStar, range); @@ -3068,7 +3074,10 @@ impl<'src> Parser<'src> { } else if is_star != Some(kind.is_star()) { mixed_except_ranges.push(handler.range()); } - handler + if handlers.is_empty() { + handlers.reserve_exact(1); + } + handlers.push(handler); }); handlers.shrink_to_fit(); @@ -4964,10 +4973,13 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Indent); let statements = if let Some(statements) = self.with_recursion(|parser| { - parser.parse_list_into_thin_vec( - RecoveryContextKind::BlockStatements, - Parser::parse_statement, - ) + let snapshot = parser.stmt_scratch.snapshot(); + parser.parse_list(RecoveryContextKind::BlockStatements, |parser| { + let statement = parser.parse_statement(); + parser.stmt_scratch.push(statement); + }); + + parser.stmt_scratch.take_thin_vec(snapshot) }) { statements } else { @@ -5205,6 +5217,9 @@ impl<'src> Parser<'src> { // uses `Parameter` (not `ParameterWithDefault`) which means that the parser cannot // recover well from `*args=(1, 2)`. let mut parameters = ast::Parameters::default(); + let parameters_snapshot = self.parameter_scratch.snapshot(); + let mut args_snapshot = None; + let mut kwonlyargs_snapshot = None; let mut seen_default_param = false; // `a=10` let mut seen_positional_only_separator = false; // `/` @@ -5231,6 +5246,9 @@ impl<'src> Parser<'src> { let star_range = parser.current_token_range(); parser.bump(TokenKind::Star); + kwonlyargs_snapshot + .get_or_insert_with(|| parser.parameter_scratch.snapshot()); + if parser.at_name_or_soft_keyword() { let param = parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::Yes, AllowContextModifier::No); let param_star_range = parser.node_range(star_range.start()); @@ -5342,7 +5360,10 @@ impl<'src> Parser<'src> { let slash_range = parser.current_token_range(); parser.bump(TokenKind::Slash); - if parameters.is_empty() { + if parser.parameter_scratch.is_empty(¶meters_snapshot) + && parameters.vararg.is_none() + && parameters.kwarg.is_none() + { // test_err params_no_arg_before_slash // def foo(/): ... // def foo(/, a): ... @@ -5382,9 +5403,11 @@ impl<'src> Parser<'src> { } if !seen_positional_only_separator { - // We should only swap if we're seeing the separator for the + // We should only split if we're seeing the separator for the // first time, otherwise it's a user error. - std::mem::swap(&mut parameters.args, &mut parameters.posonlyargs); + if kwonlyargs_snapshot.is_none() { + args_snapshot = Some(parser.parameter_scratch.snapshot()); + } seen_positional_only_separator = true; // test_ok pos_only_py38 @@ -5439,11 +5462,7 @@ impl<'src> Parser<'src> { seen_keyword_only_param_after_separator = true; } - if seen_keyword_only_separator || parameters.vararg.is_some() { - parameters.kwonlyargs.push(param); - } else { - parameters.args.push(param); - } + parser.parameter_scratch.push(param); last_keyword_only_separator_range = None; } _ => { @@ -5463,6 +5482,22 @@ impl<'src> Parser<'src> { self.add_error(ParseErrorType::ExpectedKeywordParam, star_range); } + if matches!(function_kind, FunctionKind::FunctionDef) { + self.expect(TokenKind::Rpar); + } + + if let Some(kwonlyargs_snapshot) = kwonlyargs_snapshot { + parameters.kwonlyargs = self.parameter_scratch.take_thin_vec(kwonlyargs_snapshot); + } + if let Some(args_snapshot) = args_snapshot { + parameters.args = self.parameter_scratch.take_thin_vec(args_snapshot); + parameters.posonlyargs = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } else if seen_positional_only_separator { + parameters.posonlyargs = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } else { + parameters.args = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } + // basedpython: a `context` parameter receives its implicit argument by // keyword, so it must not sit where an explicit positional argument // could land on it (or slide past it) at a call site @@ -5498,14 +5533,6 @@ impl<'src> Parser<'src> { ); } - if matches!(function_kind, FunctionKind::FunctionDef) { - self.expect(TokenKind::Rpar); - } - - parameters.args.shrink_to_fit(); - parameters.kwonlyargs.shrink_to_fit(); - parameters.posonlyargs.shrink_to_fit(); - parameters.range = self.node_range(start); parameters @@ -5957,12 +5984,7 @@ impl<'src> Parser<'src> { /// For now, don't recover when parsing clause headers, but add the terminator tokens (e.g. /// `Else`) to the recovery context so that expression recovery stops when it encounters an /// `else` token. - fn parse_clauses( - &mut self, - clause: Clause, - mut parse_clause: impl FnMut(&mut Parser<'src>) -> T, - ) -> Vec { - let mut clauses = Vec::new(); + fn parse_clauses(&mut self, clause: Clause, mut parse_clause: impl FnMut(&mut Parser<'src>)) { let mut progress = ParserProgress::default(); let recovery_kind = match clause { @@ -5979,15 +6001,10 @@ impl<'src> Parser<'src> { while recovery_kind.is_list_element(self) { progress.assert_progressing(self); - if clauses.is_empty() { - clauses.reserve_exact(1); - } - clauses.push(parse_clause(self)); + parse_clause(self); } self.recovery_context = saved_context; - - clauses } } diff --git a/crates/ruff_python_parser/src/parser/tests.rs b/crates/ruff_python_parser/src/parser/tests.rs index 179da81d6e..4a180c7c4e 100644 --- a/crates/ruff_python_parser/src/parser/tests.rs +++ b/crates/ruff_python_parser/src/parser/tests.rs @@ -528,6 +528,19 @@ fn nfkc_normalizes_names() { assert_eq!(name.id.as_str(), "C"); } +#[test] +fn nfkc_normalizes_dotted_names() { + let suite = parse_module("import 𝒞.𝒟").unwrap().into_suite(); + let [Stmt::Import(import)] = suite.as_slice() else { + panic!("expected a single import statement, got {suite:?}"); + }; + let [alias] = import.names.as_slice() else { + panic!("expected a single import alias, got {:?}", import.names); + }; + + assert_eq!(alias.name.id.as_str(), "C.D"); +} + #[test] fn number_values() { let cases = [ diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index e7bea78d47..ed753c2ed0 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index deba3ddcb4..bd19e639c7 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_semantic). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/src/cfg/visualize.rs b/crates/ruff_python_semantic/src/cfg/visualize.rs index 64c9b18106..fe861c122b 100644 --- a/crates/ruff_python_semantic/src/cfg/visualize.rs +++ b/crates/ruff_python_semantic/src/cfg/visualize.rs @@ -24,7 +24,7 @@ trait MermaidGraph<'a>: DirectedGraph<'a> { let num_nodes = self.num_nodes(); for idx in 0..num_nodes { let node = Self::Node::new(idx); - graph.push(format!("\tnode{}{}", idx, &self.draw_node(node))); + graph.push(format!("\tnode{}{}", idx, self.draw_node(node))); } // Draw edges diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index f0962516a2..61e9b6309f 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index 3af6d34366..720253982a 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_stdlib). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index 73ff8f20b6..e00ccd86b4 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index a2e29f72ca..ea990d2d33 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_python_trivia). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/src/whitespace.rs b/crates/ruff_python_trivia/src/whitespace.rs index c9c4b7294b..e36f570caa 100644 --- a/crates/ruff_python_trivia/src/whitespace.rs +++ b/crates/ruff_python_trivia/src/whitespace.rs @@ -1,6 +1,50 @@ +use std::borrow::Cow; + use ruff_source_file::LineRanges; use ruff_text_size::{TextRange, TextSize}; +/// Expands tabs to the next eight-column tab stop, matching Python's `str.expandtabs`. +pub fn expand_tabs(source: &str) -> Cow<'_, str> { + const TAB_SIZE: usize = 8; + + if !source.contains('\t') { + return Cow::Borrowed(source); + } + + let mut expanded = String::with_capacity(source.len()); + let mut column = 0; + + for character in source.chars() { + match character { + '\t' => { + let spaces = tab_offset(column, TAB_SIZE); + expanded.extend(std::iter::repeat_n(' ', spaces)); + column += spaces; + } + '\r' | '\n' => { + expanded.push(character); + column = 0; + } + _ => { + expanded.push(character); + column += 1; + } + } + } + + Cow::Owned(expanded) +} + +/// Returns the number of columns from `column` to the next tab stop. +pub const fn tab_offset(column: usize, tab_size: usize) -> usize { + tab_size - column % tab_size +} + +/// Returns the number of columns from `column` to the next tab stop using `u32` values. +pub const fn tab_offset_u32(column: u32, tab_size: u32) -> u32 { + tab_size - column % tab_size +} + /// Extract the leading indentation from a line. pub fn indentation_at_offset(offset: TextSize, source: &str) -> Option<&str> { let line_start = source.line_start(offset); @@ -78,3 +122,37 @@ impl PythonWhitespace for str { self.trim_end_matches(is_python_whitespace) } } + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::{expand_tabs, tab_offset, tab_offset_u32}; + + #[test] + fn tab_expansion_borrows_unchanged_text() { + assert!(matches!(expand_tabs("unchanged"), Cow::Borrowed(_))); + } + + #[test] + fn tab_expansion_allocates_changed_text() { + let expanded = expand_tabs(" \tvalue"); + + assert!(matches!(&expanded, Cow::Owned(_))); + assert_eq!(expanded, " value"); + } + + #[test] + fn tab_offset_advances_to_next_stop() { + assert_eq!(tab_offset(0, 8), 8); + assert_eq!(tab_offset(2, 8), 6); + assert_eq!(tab_offset(8, 8), 8); + } + + #[test] + fn u32_tab_offset_advances_to_next_stop() { + assert_eq!(tab_offset_u32(0, 8), 8); + assert_eq!(tab_offset_u32(2, 8), 6); + assert_eq!(tab_offset_u32(8, 8), 8); + } +} diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index 6fc498c503..df3e113867 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index dd5fef539f..eb36cea302 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_ranged_value). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 932e088b1d..808f812876 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 2eaf34042d..9f1ed8c62f 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_server). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/src/server/api/requests/code_action_resolve.rs b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs index 9e34ab6be0..4ccc3cab4d 100644 --- a/crates/ruff_server/src/server/api/requests/code_action_resolve.rs +++ b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs @@ -148,6 +148,13 @@ pub(super) fn organize_imports_edit( linter_settings.rules = [ Rule::UnsortedImports, // I001 Rule::MissingRequiredImport, // I002 + // Note: ModuleImportNotAtTopOfFile's fixes are unsafe. We include them + // here in order to match isort's behaviour and what we believe + // developers want. Since the fixes are unsafe, we're relying on this + // edit action not performing them unless the user has opted-in to these + // fixes in their settings (i.e: `extend-safe-fixes` in the + // `pyproject.toml` or similar). + Rule::ModuleImportNotAtTopOfFile, // E402 ] .into_iter() .collect(); diff --git a/crates/ruff_server/src/session/client.rs b/crates/ruff_server/src/session/client.rs index 1f2bfef5a1..e2896e2e57 100644 --- a/crates/ruff_server/src/session/client.rs +++ b/crates/ruff_server/src/session/client.rs @@ -3,7 +3,6 @@ use crate::server::{ConnectionSender, Event, MainLoopSender}; use anyhow::{Context, anyhow}; use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError}; use serde_json::Value; -use std::any::TypeId; use std::fmt::Display; pub(crate) type ClientResponseHandler = Box; @@ -53,8 +52,8 @@ impl Client { tracing::debug_span!("client_response", id=%response.id, method = %R::METHOD) .entered(); - match (response.error, response.result) { - (Some(err), _) => { + match response.response_result { + Err(err) => { tracing::error!( "Got an error from the client (code {code}, method {method}): {message}", code = err.code, @@ -62,7 +61,7 @@ impl Client { method = R::METHOD ); } - (None, Some(response)) => match serde_json::from_value(response) { + Ok(response) => match serde_json::from_value(response) { Ok(response) => response_handler(client, response), Err(error) => { tracing::error!( @@ -71,21 +70,6 @@ impl Client { ); } }, - (None, None) => { - if TypeId::of::() == TypeId::of::<()>() { - // We can't call `response_handler(())` directly here, but - // since we _know_ the type expected is `()`, we can use - // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, - // so this branch works in the general case but we'll only - // hit it if the concrete type is `()`, so the `unwrap()` is safe here. - response_handler(client, serde_json::from_value(Value::Null).unwrap()); - } else { - tracing::error!( - "Invalid client response: did not contain a result or error (method={method})", - method = R::METHOD - ); - } - } } }); @@ -173,8 +157,7 @@ impl Client { ) -> crate::Result<()> { let response = lsp_server::Response { id, - result: None, - error: Some(error), + response_result: Err(error), }; self.main_loop_sender @@ -236,8 +219,7 @@ impl Client { self.client_sender .send(Message::Response(lsp_server::Response { id, - result: None, - error: Some(error), + response_result: Err(error), }))?; } diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 07c67d29ee..6349ed3c0c 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -113,9 +113,6 @@ pub(crate) enum AwaitResponseError { #[error("request failed because the server replied with an error: {0:?}")] RequestFailed(ResponseError), - #[error("malformed response message with both result and error: {0:#?}")] - MalformedResponse(Box), - #[error("received multiple responses for the same request ID: {0:#?}")] MultipleResponses(Box<[Response]>), @@ -418,24 +415,13 @@ impl TestServer { let response = responses.pop().unwrap(); - match response { - Response { - error: None, - result: Some(result), - .. - } => { + match response.response_result { + Ok(result) => { return Ok(serde_json::from_value::(result)?); } - Response { - error: Some(err), - result: None, - .. - } => { + Err(err) => { return Err(AwaitResponseError::RequestFailed(err)); } - response => { - return Err(AwaitResponseError::MalformedResponse(Box::new(response))); - } } } @@ -521,7 +507,7 @@ impl TestServer { { panic!( "Received multiple publish diagnostic notifications for {uri}: ({existing:#?})", - uri = ¬ification.uri + uri = notification.uri ); } } diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index e8167e479d..b713313d5a 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 2edc9a4b4b..693ee2da7a 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_source_file). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index 1da6e86d52..fcd8305cde 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index d12c578060..21887ec343 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_text_size). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 3cc0288120..f7c7ac1871 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.20" +version = "0.15.22" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index bc5edd6e5f..b58015c3f6 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.20) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_wasm). +This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index e384a0ab3b..7aaae335ca 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.3" +version = "0.0.5" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index 21f59a4b52..6a9c1eb472 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.3) is a component of [Ruff 0.15.20](https://crates.io/crates/ruff/0.15.20). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.20/crates/ruff_workspace). +This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index f6ec9bd372..cd19803f52 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -257,15 +257,16 @@ pub struct Options { /// A list of file patterns to include when linting. /// /// Inclusion are based on globs, and should be single-path patterns, like - /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is - /// included here not for configuration but because we lint whether e.g. the - /// `[project]` matches the schema. + /// `*.pyw`, to include any file with the `.pyw` extension. + /// `pyproject.toml`, `ruff.toml`, and `.ruff.toml` are included here not for + /// configuration but because we lint whether e.g. the `[project]` matches + /// the schema in `pyproject.toml` or that rule names are used as selectors. /// /// Notebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+. /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). #[option( - default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml"]"#, + default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml"]"#, value_type = "list[str]", example = r#" include = ["*.py"] diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 0d7adab236..5ebf2f3f46 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -1050,7 +1050,7 @@ mod tests { fn exclusions() { let project_root = Path::new("/tmp/"); - let path = Path::new("foo").absolutize_from(project_root).unwrap(); + let path = Path::new("foo").absolutize_from(project_root); let exclude = FilePattern::User("foo".to_string(), GlobPath::normalize("foo", project_root)); let file_path = &path; @@ -1061,7 +1061,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); + let path = Path::new("foo/bar").absolutize_from(project_root); let exclude = FilePattern::User("bar".to_string(), GlobPath::normalize("bar", project_root)); let file_path = &path; @@ -1072,9 +1072,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar/baz.py") - .absolutize_from(project_root) - .unwrap(); + let path = Path::new("foo/bar/baz.py").absolutize_from(project_root); let exclude = FilePattern::User( "baz.py".to_string(), GlobPath::normalize("baz.py", project_root), @@ -1087,7 +1085,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); + let path = Path::new("foo/bar").absolutize_from(project_root); let exclude = FilePattern::User( "foo/bar".to_string(), GlobPath::normalize("foo/bar", project_root), @@ -1100,9 +1098,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar/baz.py") - .absolutize_from(project_root) - .unwrap(); + let path = Path::new("foo/bar/baz.py").absolutize_from(project_root); let exclude = FilePattern::User( "foo/bar/baz.py".to_string(), GlobPath::normalize("foo/bar/baz.py", project_root), @@ -1115,9 +1111,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar/baz.py") - .absolutize_from(project_root) - .unwrap(); + let path = Path::new("foo/bar/baz.py").absolutize_from(project_root); let exclude = FilePattern::User( "foo/bar/*.py".to_string(), GlobPath::normalize("foo/bar/*.py", project_root), @@ -1130,9 +1124,7 @@ mod tests { &make_exclusion(exclude), )); - let path = Path::new("foo/bar/baz.py") - .absolutize_from(project_root) - .unwrap(); + let path = Path::new("foo/bar/baz.py").absolutize_from(project_root); let exclude = FilePattern::User("baz".to_string(), GlobPath::normalize("baz", project_root)); let file_path = &path; diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 29ae23f5d2..37b3bccb3e 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -145,6 +145,8 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("*.byi"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("**/ruff.toml"), + FilePattern::Builtin("**/.ruff.toml"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), @@ -154,6 +156,8 @@ pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.byi"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("**/ruff.toml"), + FilePattern::Builtin("**/.ruff.toml"), FilePattern::Builtin("*.md"), ]; diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 25c6fd76a4..98d077d2db 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -230,6 +230,112 @@ Defaults to `true`. --- +### `strict-equality-semantics` + +Configure ty's behavior regarding type inference and narrowing of equality +checks. Defaults to `false`. + +By default, ty makes various assumptions about equality checks that match the +intuitions of most Python programmers, but may not be fully sound in all situations. +Enabling this option makes ty more conservative about these assumptions, making it +less likely to infer `Literal[True]` or `Literal[False]` as the result of an +equality check. This has various effects on type checking, including fewer type +narrowing opportunities and more conservative assumptions regarding control flow. + +One way in which ty will by default make unsound assumptions is by narrowing an +object `x` of type `str` to `Literal["a"]` after an `if x == "a"` check. This is +unsound because a subclass of `str` with value `"a"` will (by default) compare equal +to `"a"`, but will not be of type `Literal["a"]`: + +```pycon +>>> # `Literal["a"]` can only be inhabited by instances of exactly `str`, not +>>> # subclasses, but str subclasses compare equal by default: +>>> class StringSubclass(str): ... +... +>>> StringSubclass("a") == "a" +True +>>> +>>> # This also applies to `StrEnum`s: +>>> from enum import StrEnum +>>> class MyEnum(StrEnum): +... A = "a" +... +>>> MyEnum.A == "a" +True +``` + +Enabling this option prevents the unsound narrowing of `x` to `Literal["a"]`, +and instead keeps it as `str`: + +```python +from typing import Literal + +def parse(value: str) -> Literal["a"] | None: + # with `strict-equality-semantics = true`, no narrowing will occur here, + # and an error will be emitted on the `return` statement. + if value == "a": + return value + return None +``` + +Another assumption ty makes by default is that subclasses will never override `__eq__` or +`__ne__`. This allows ty to narrow the following union based on an equality check, despite +the fact that an instance of a subclass of `Foo` could compare equal to `None`, and it's +perfectly valid to pass an instance of a subclass into the `x` parameter of this function: + +```python +def narrow(x: Foo | None, other: Foo) -> None: + if x == other: + # with this option enabled, `x` will still have type `Foo | None` here, + # since it is legal to subclass `Foo` and override its `__eq__` method. + reveal_type(x) +``` + +Many operations in Python implicitly call `__eq__` under the hood; enabling this option +will also impact those operations. For example, this option will also impact narrowing from +`in` checks, and narrowing in `match` statements that use value patterns: + +```python +def narrow_in(x: Foo | None, other: list[Foo]) -> None: + if x in other: + # with this option enabled, `x` will still have type `Foo | None` here, + # since the `in` operator implicitly calls `__eq__` on each element of `other`. + reveal_type(x) + + +def narrow_match(x: str) -> None: + match x: + case "a": + # with this option enabled, `x` will still have type `str` here, + # since this `case` branch will be taken by any object that compares + # equal to `"a"`, including subclasses of `str`. + reveal_type(x) +``` + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.analysis] + # Preserve broad builtin types instead of narrowing them to literals + strict-equality-semantics = true + ``` + +=== "ty.toml" + + ```toml + [analysis] + # Preserve broad builtin types instead of narrowing them to literals + strict-equality-semantics = true + ``` + +--- + ## `environment` ### `extra-paths` @@ -794,6 +900,112 @@ Defaults to `true`. --- +#### `strict-equality-semantics` + +Configure ty's behavior regarding type inference and narrowing of equality +checks. Defaults to `false`. + +By default, ty makes various assumptions about equality checks that match the +intuitions of most Python programmers, but may not be fully sound in all situations. +Enabling this option makes ty more conservative about these assumptions, making it +less likely to infer `Literal[True]` or `Literal[False]` as the result of an +equality check. This has various effects on type checking, including fewer type +narrowing opportunities and more conservative assumptions regarding control flow. + +One way in which ty will by default make unsound assumptions is by narrowing an +object `x` of type `str` to `Literal["a"]` after an `if x == "a"` check. This is +unsound because a subclass of `str` with value `"a"` will (by default) compare equal +to `"a"`, but will not be of type `Literal["a"]`: + +```pycon +>>> # `Literal["a"]` can only be inhabited by instances of exactly `str`, not +>>> # subclasses, but str subclasses compare equal by default: +>>> class StringSubclass(str): ... +... +>>> StringSubclass("a") == "a" +True +>>> +>>> # This also applies to `StrEnum`s: +>>> from enum import StrEnum +>>> class MyEnum(StrEnum): +... A = "a" +... +>>> MyEnum.A == "a" +True +``` + +Enabling this option prevents the unsound narrowing of `x` to `Literal["a"]`, +and instead keeps it as `str`: + +```python +from typing import Literal + +def parse(value: str) -> Literal["a"] | None: + # with `strict-equality-semantics = true`, no narrowing will occur here, + # and an error will be emitted on the `return` statement. + if value == "a": + return value + return None +``` + +Another assumption ty makes by default is that subclasses will never override `__eq__` or +`__ne__`. This allows ty to narrow the following union based on an equality check, despite +the fact that an instance of a subclass of `Foo` could compare equal to `None`, and it's +perfectly valid to pass an instance of a subclass into the `x` parameter of this function: + +```python +def narrow(x: Foo | None, other: Foo) -> None: + if x == other: + # with this option enabled, `x` will still have type `Foo | None` here, + # since it is legal to subclass `Foo` and override its `__eq__` method. + reveal_type(x) +``` + +Many operations in Python implicitly call `__eq__` under the hood; enabling this option +will also impact those operations. For example, this option will also impact narrowing from +`in` checks, and narrowing in `match` statements that use value patterns: + +```python +def narrow_in(x: Foo | None, other: list[Foo]) -> None: + if x in other: + # with this option enabled, `x` will still have type `Foo | None` here, + # since the `in` operator implicitly calls `__eq__` on each element of `other`. + reveal_type(x) + + +def narrow_match(x: str) -> None: + match x: + case "a": + # with this option enabled, `x` will still have type `str` here, + # since this `case` branch will be taken by any object that compares + # equal to `"a"`, including subclasses of `str`. + reveal_type(x) +``` + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.overrides.analysis] + # Preserve broad builtin types instead of narrowing them to literals + strict-equality-semantics = true + ``` + +=== "ty.toml" + + ```toml + [overrides.analysis] + # Preserve broad builtin types instead of narrowing them to literals + strict-equality-semantics = true + ``` + +--- + ## `src` ### `exclude` diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 78a209e216..07129b31e7 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -54,7 +54,7 @@ class Derived(Base): # error Default level: error · Added in 0.0.61 · Related issues · -View source +View source @@ -87,7 +87,7 @@ f(1, b=s1) # ok — explicit Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -122,7 +122,7 @@ extension list: Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -186,7 +186,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -233,7 +233,7 @@ def _(x: int): Default level: ignore · Added in 0.0.57 · Related issues · -View source +View source @@ -269,7 +269,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -324,7 +324,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -352,7 +352,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -387,7 +387,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -421,7 +421,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -456,7 +456,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -492,7 +492,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -528,7 +528,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -565,7 +565,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -604,7 +604,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -637,7 +637,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -668,7 +668,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -711,7 +711,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -761,7 +761,7 @@ def bar() -> str: # error: [empty-body] Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -822,7 +822,7 @@ def h(x: object): Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def foo() -> "intt\b": ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -933,7 +933,7 @@ def f(local fn: () -> None): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -974,7 +974,7 @@ for x in [1, 2, 3]: Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -1014,7 +1014,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -1049,7 +1049,7 @@ def my_function() -> int: Default level: error · Added in 0.0.40 · Related issues · -View source +View source @@ -1084,7 +1084,7 @@ let a = 1 Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -1121,7 +1121,7 @@ INITIALIZED_CONSTANT: Final[int] = 1 Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1200,7 +1200,7 @@ def test() -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1236,7 +1236,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1266,7 +1266,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1303,7 +1303,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1404,7 +1404,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1436,7 +1436,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1467,7 +1467,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1525,7 +1525,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1571,7 +1571,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1613,7 +1613,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1640,7 +1640,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1670,7 +1670,7 @@ with 1: # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1723,7 +1723,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1759,7 +1759,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1791,7 +1791,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1848,7 +1848,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1912,7 +1912,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1965,7 +1965,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -1998,7 +1998,7 @@ extension list[T: int]: # error: `list` declares no type parameter `T` Default level: error · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -2027,7 +2027,7 @@ Author.objects.filter(name__startswith=1) # error: lookup wants `str` Default level: error · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -2063,7 +2063,7 @@ def test_user(user: int) -> None: # error: fixture provides `str` Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -2114,7 +2114,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2163,7 +2163,7 @@ class D(Generic[U, T]): ... # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -2225,7 +2225,7 @@ x: G[int] Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2259,7 +2259,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -2307,7 +2307,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -2369,7 +2369,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2409,7 +2409,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2459,7 +2459,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2494,7 +2494,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2612,7 +2612,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2669,7 +2669,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -2717,7 +2717,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -2755,7 +2755,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2812,7 +2812,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2841,7 +2841,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -2874,7 +2874,7 @@ def test_add(a: int, b: int) -> None: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2910,7 +2910,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2946,7 +2946,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3017,7 +3017,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3049,7 +3049,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3160,7 +3160,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -3211,7 +3211,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -3252,7 +3252,7 @@ NewAlias = TypeAliasType(get_name(), int) # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3319,7 +3319,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3352,7 +3352,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3382,60 +3382,13 @@ b: Annotated[int] # error [type expressions]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions -## `invalid-type-guard-call` - - -Default level: error · -Added in 0.0.1-alpha.11 · -Related issues · -View source - - - -**What it does** - - -Checks for type guard function calls without a valid target. - -**Why is this bad?** - - -The first non-keyword non-variadic argument to a type guard function -is its target and must map to a symbol. - -Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like -expressions are invalid as narrowing targets. - -**Examples** - - -```toml -[environment] -python-version = "3.13" -``` - -```python -from typing import TypeIs - - -def is_int(value: object = object()) -> TypeIs[int]: - return isinstance(value, int) - - -# no positional narrowing target -is_int() # error - -# narrowing target passed by keyword -is_int(value=1) # error -``` - ## `invalid-type-guard-definition` Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3492,7 +3445,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3536,7 +3489,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3593,7 +3546,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3635,7 +3588,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -3671,7 +3624,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3714,7 +3667,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -3749,7 +3702,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -3784,7 +3737,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3851,7 +3804,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3901,7 +3854,7 @@ def g(arg: object): Default level: warn · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -3932,7 +3885,7 @@ def f(s: str): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -3975,7 +3928,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4006,7 +3959,7 @@ func() # error Default level: error · Added in 0.0.61 · Related issues · -View source +View source @@ -4037,7 +3990,7 @@ f(1) # ok — `s` is passed implicitly Default level: warn · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -4065,7 +4018,7 @@ from django.db import models # warning: install `django-stubs` for precise type Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -4124,7 +4077,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -4163,7 +4116,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -4202,7 +4155,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4240,7 +4193,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -4278,7 +4231,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -4307,7 +4260,7 @@ def f(a: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4336,7 +4289,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4364,7 +4317,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4393,7 +4346,7 @@ def f(once done: () -> None): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4421,7 +4374,7 @@ def f(once done: () -> None): Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -4459,7 +4412,7 @@ def f(x: int?): Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -4496,7 +4449,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -4533,7 +4486,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4564,7 +4517,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4595,7 +4548,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4634,7 +4587,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4673,7 +4626,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4719,7 +4672,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -4751,7 +4704,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4788,7 +4741,7 @@ print(x) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4814,6 +4767,46 @@ private type Key = str | int from helpers import Key # error: `Key` is private to `helpers` ``` +## `pydantic-discarded-extra-argument` + + +Default level: warn · +Added in 0.0.60 · +Related issues · +View source + + + +**What it does** + + +Checks for extra keyword arguments that Pydantic silently discards when a model uses +`extra="ignore"`, either implicitly or explicitly. + +**Why is this bad?** + + +A discarded argument has no effect on the constructed model, but it may indicate a misspelled field +name or an incorrect assumption about the model's schema. + +**Example** + + +```python {data-mdtest="ignore"} +from pydantic import BaseModel + + +class User(BaseModel): + name: str + admin: bool = False + + +user = User(name="Alice", admni=True) # error: [pydantic-discarded-extra-argument] +``` + +If the field name has been misspelled, fix the typo. Otherwise, consider removing the extra argument, +or explicitly configure the model with `extra="allow"`. + ## `raw-string-type-annotation` @@ -4855,7 +4848,7 @@ def test() -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4890,7 +4883,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -4928,7 +4921,7 @@ class C: Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -4961,7 +4954,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -5005,7 +4998,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5040,7 +5033,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -5091,7 +5084,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5125,7 +5118,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5157,7 +5150,7 @@ class Circle(Shape): ... # error: `Shape` is sealed in another workspace Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -5197,7 +5190,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5227,7 +5220,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5266,7 +5259,7 @@ def find(items: list[int]) -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5301,7 +5294,7 @@ f: # error: the block returns `None`, not `str` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5340,7 +5333,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -5371,7 +5364,7 @@ class User(BaseModel): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5429,7 +5422,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -5473,7 +5466,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5502,7 +5495,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5533,7 +5526,7 @@ f(x=1, y=2) # error Default level: ignore · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -5569,7 +5562,7 @@ def test_thing(no_such_fixture) -> None: # requests an unknown fixture Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5602,7 +5595,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -5677,7 +5670,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5706,7 +5699,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5734,7 +5727,7 @@ print(x) # error Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -5776,7 +5769,7 @@ g(1) # ok — transpiles to g[int](1) Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -5823,7 +5816,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5872,7 +5865,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -5919,7 +5912,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5952,7 +5945,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -5990,7 +5983,7 @@ async def main() -> None: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6031,7 +6024,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments. Default level: warn · Added in 0.0.14 · Related issues · -View source +View source @@ -6072,7 +6065,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -6151,7 +6144,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 807367a28f..e5d9a15532 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -6,7 +6,7 @@ mod python_version; mod rule; mod version; -use std::fmt::Write; +use std::io::{BufWriter, Write}; use std::process::{ExitCode, Termination}; use std::sync::Mutex; @@ -25,7 +25,6 @@ use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ruff_db::{STACK_SIZE, max_parallelism}; use ruff_diagnostics::Applicability; use salsa::Database; -use ty_project::metadata::options::ProjectOptionsOverrides; use ty_project::metadata::settings::TerminalSettings; use ty_project::watch::ProjectWatcher; use ty_project::{CollectReporter, Db, watch}; @@ -165,8 +164,7 @@ fn run_generate_api_file( }), ..ty_project::metadata::options::Options::default() }; - let overrides = ProjectOptionsOverrides::new(None, cli_options); - project_metadata.apply_overrides(&overrides); + project_metadata.apply_override_options(cli_options); let db = ProjectDatabase::fallible(project_metadata, system)?; let project = db.project(); @@ -297,8 +295,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result { project_metadata.apply_configuration_files(&system)?; - let project_options_overrides = ProjectOptionsOverrides::new(config_file, args.into_options()); - project_metadata.apply_overrides(&project_options_overrides); + project_metadata.apply_override_options(args.into_options()); let mut db = ProjectDatabase::fallible(project_metadata, system)?; let project = db.project(); @@ -316,6 +313,9 @@ fn run_check(args: CheckCommand) -> anyhow::Result { ruff_db::disable_lru(&mut db); } + // The CLI never opens files, so this is safe even where the freeze below isn't + db.freeze_open_files(); + // A one-shot check never mutates these heavily read inputs, so freezing them avoids recording // unnecessary Salsa dependencies. Watch mode updates inputs incrementally, fix modes apply // source-text overrides, and memory reports measure the database without this optimization, so @@ -324,8 +324,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result { db.freeze(); } - let (main_loop, main_loop_cancellation_token) = - MainLoop::new(mode, project_options_overrides, printer); + let (main_loop, main_loop_cancellation_token) = MainLoop::new(mode, printer); // Listen to Ctrl+C and abort the watch mode. let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token)); @@ -415,8 +414,6 @@ struct MainLoop { /// Interface for displaying information to the user. 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. @@ -424,11 +421,7 @@ struct MainLoop { } impl MainLoop { - fn new( - mode: MainLoopMode, - project_options_overrides: ProjectOptionsOverrides, - printer: Printer, - ) -> (Self, MainLoopCancellationToken) { + fn new(mode: MainLoopMode, printer: Printer) -> (Self, MainLoopCancellationToken) { let (sender, receiver) = crossbeam_channel::bounded(10); let cancellation_token_source = CancellationTokenSource::new(); @@ -440,7 +433,6 @@ impl MainLoop { sender: sender.clone(), receiver, watcher: None, - project_options_overrides, printer, cancellation_token, }, @@ -611,7 +603,7 @@ impl MainLoop { revision += 1; // Automatically cancels any pending queries and waits for them to complete. - db.apply_changes(&changes, Some(&self.project_options_overrides)); + db.apply_changes(&changes); if let Some(watcher) = self.watcher.as_mut() { watcher.update(db); } @@ -651,11 +643,12 @@ impl MainLoop { diagnostics => { let diagnostics_count = diagnostics.len(); - let mut stdout = self.printer.stream_for_details().lock(); + let stdout = self.printer.stream_for_details().lock(); // Only render diagnostics if they're going to be displayed, since doing // so is expensive. if stdout.is_enabled() { + let mut stdout = BufWriter::new(stdout); let display_config = DisplayDiagnosticConfig::new("ty") .format(terminal_settings.output_format.into()) .color(colored::control::SHOULD_COLORIZE.should_colorize()) @@ -668,6 +661,7 @@ impl MainLoop { "{}", DisplayDiagnostics::new(db, &display_config, diagnostics) )?; + stdout.flush()?; } if !self.cancellation_token.is_cancelled() && is_human_readable { diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 547da0cb82..4375444983 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -22,6 +22,17 @@ pub fn main() -> ExitStatus { run().unwrap_or_else(|error| { use io::Write; + // Exit "gracefully" on broken pipe errors. + // + // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 + if error.chain().any(|cause| { + cause + .downcast_ref::() + .is_some_and(|ioerr| ioerr.kind() == io::ErrorKind::BrokenPipe) + }) { + return ExitStatus::Success; + } + // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. let mut stderr = io::stderr().lock(); @@ -32,15 +43,6 @@ pub fn main() -> ExitStatus { // the configuration it is help to chain errors ("resolving configuration failed" -> // "failed to read file: subdir/pyproject.toml") for cause in error.chain() { - // Exit "gracefully" on broken pipe errors. - // - // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 - if let Some(ioerr) = cause.downcast_ref::() { - if ioerr.kind() == io::ErrorKind::BrokenPipe { - return ExitStatus::Success; - } - } - writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); } diff --git a/crates/ty/src/printer.rs b/crates/ty/src/printer.rs index 39c113f0f7..d72e1ebb5a 100644 --- a/crates/ty/src/printer.rs +++ b/crates/ty/src/printer.rs @@ -147,10 +147,11 @@ impl Stdout { self } - fn handle(&mut self) -> Box { + #[inline] + fn with_handle(&mut self, callback: impl FnOnce(&mut dyn std::io::Write) -> T) -> T { match self.lock.as_mut() { - Some(lock) => Box::new(lock), - None => Box::new(std::io::stdout()), + Some(lock) => callback(lock), + None => callback(&mut std::io::stdout()), } } @@ -162,26 +163,14 @@ impl Stdout { impl std::io::Write for Stdout { fn write(&mut self, buf: &[u8]) -> std::io::Result { match self.status { - StreamStatus::Enabled => self.handle().write(buf), + StreamStatus::Enabled => self.with_handle(|handle| handle.write(buf)), StreamStatus::Disabled => Ok(buf.len()), } } fn flush(&mut self) -> std::io::Result<()> { match self.status { - StreamStatus::Enabled => self.handle().flush(), - StreamStatus::Disabled => Ok(()), - } - } -} - -impl std::fmt::Write for Stdout { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - match self.status { - StreamStatus::Enabled => { - let _ = write!(self.handle(), "{s}"); - Ok(()) - } + StreamStatus::Enabled => self.with_handle(|handle| handle.flush()), StreamStatus::Disabled => Ok(()), } } diff --git a/crates/ty/tests/cli/api_lockfile.rs b/crates/ty/tests/cli/api_lockfile.rs index 594277afe9..3dcbf196c9 100644 --- a/crates/ty/tests/cli/api_lockfile.rs +++ b/crates/ty/tests/cli/api_lockfile.rs @@ -41,7 +41,7 @@ def _private() -> None: exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.3 + #tool:by=0.0.5 #python:default #modules:1 module.CONST:v=builtins.int @@ -90,7 +90,7 @@ class Dog(Animal): exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.3 + #tool:by=0.0.5 #python:default #modules:2 base.Animal.speak:d(self:base.Animal)->builtins.str @@ -127,7 +127,7 @@ def f(a: int, b: str = '', /, c: float = 0.0, *args: bytes, d: bool = False, **k exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.3 + #tool:by=0.0.5 #python:default #modules:1 sigs.f:d(a:builtins.int,b:builtins.str=,/,c:builtins.float | builtins.int=,*args:builtins.bytes,d:builtins.bool=,**kwargs:builtins.int)->None @@ -172,7 +172,7 @@ class D[T, U]: exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.3 + #tool:by=0.0.5 #python:default #modules:1 g.A.f:d(self:Self)->T @@ -227,7 +227,7 @@ class Cell(Generic[T]): exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.3 + #tool:by=0.0.5 #python:default #modules:1 ex.Box.get:d(self:Self)->T_co diff --git a/crates/ty/tests/cli/fixes.rs b/crates/ty/tests/cli/fixes.rs index b25816eb51..6f5f3233fd 100644 --- a/crates/ty/tests/cli/fixes.rs +++ b/crates/ty/tests/cli/fixes.rs @@ -46,6 +46,61 @@ fn add_ignore() -> anyhow::Result<()> { Ok(()) } +#[test] +fn add_ignore_keeps_nested_blanket_suppression_used() -> anyhow::Result<()> { + let case = CliTest::with_file( + "nested.py", + r#" + def f(value: int) -> int: + return value + + seen_code = True + # ty: ignore[] + values = [ + # ty: ignore[blanket-ignore-comment] + # ty: ignore + f("bad"), + # ty: ignore + missing, + ] + "#, + )?; + + assert_cmd_snapshot!( + case.command() + .arg("--add-ignore") + .arg("--warn") + .arg("blanket-ignore-comment"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + Added 1 ignore comment + + ----- stderr ----- + " + ); + + assert_snapshot!(fs::read_to_string(case.root().join("nested.py"))?, @r#" + + def f(value: int) -> int: + return value + + seen_code = True + # ty: ignore[blanket-ignore-comment] + values = [ + # ty: ignore[blanket-ignore-comment] + # ty: ignore + f("bad"), + # ty: ignore + missing, + ] + "#); + + Ok(()) +} + #[test] fn add_ignore_unfixable() -> anyhow::Result<()> { let case = CliTest::with_files([ @@ -117,16 +172,20 @@ fn fix() -> anyhow::Result<()> { "unused_ignore.py", r#" x = 1 # ty: ignore[unresolved-reference] + values = [ + # ty: ignore[] + 1, + ] "#, )?; assert_cmd_snapshot!( case.command().arg("--fix").arg("--warn").arg("unused-ignore-comment"), - @r" + @" success: true exit_code: 0 ----- stdout ----- - Found 1 diagnostic (1 fixed, 0 remaining). + Found 2 diagnostics (2 fixed, 0 remaining). ----- stderr ----- " @@ -134,8 +193,12 @@ fn fix() -> anyhow::Result<()> { assert_snapshot!( fs::read_to_string(case.root().join("unused_ignore.py"))?, - @r" + @" + x = 1 + values = [ + 1, + ] " ); diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index 9d88e5fd6e..c3f70cd126 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -7,6 +7,7 @@ mod fixes; mod python_environment; mod rule; mod rule_selection; +mod scripts; use anyhow::Context as _; use insta::Settings; diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index c5d705235d..0dfdccbfae 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -410,6 +410,55 @@ fn overrides_precedence() -> anyhow::Result<()> { Ok(()) } +/// Multiple matching overrides inherit global options from higher-precedence layers. +#[test] +fn multiple_overrides_inherit_cli_rules() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [[tool.ty.overrides]] + include = ["test.py"] + [tool.ty.overrides.rules] + division-by-zero = "warn" + + [[tool.ty.overrides]] + include = ["test.py"] + [tool.ty.overrides.rules] + possibly-unresolved-reference = "ignore" + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + prin(y) + "#, + ), + ])?; + + assert_cmd_snapshot!( + case.command().args(["--ignore", "unresolved-reference"]), + @" + success: false + exit_code: 1 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + + Found 1 diagnostic + + ----- stderr ----- + " + ); + + Ok(()) +} + /// Override with exclude patterns #[test] fn overrides_exclude() -> anyhow::Result<()> { diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs new file mode 100644 index 0000000000..885340644e --- /dev/null +++ b/crates/ty/tests/cli/scripts.rs @@ -0,0 +1,415 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +#[test] +fn project_settings_and_overrides_do_not_apply() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + unresolved-reference = "ignore" + + [[tool.ty.overrides]] + include = ["script.py"] + + [tool.ty.overrides.rules] + unresolved-reference = "error" + "#, + ), + ( + "script.py", + r#" + # /// script + # [tool.ty.rules] + # unresolved-reference = "warn" + # /// + + print(missing) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `missing` used when not defined + --> script.py:7:7 + | + 7 | print(missing) + | ^^^^^^^ + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn metadata_without_tool_ty_uses_default_settings() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + all = "ignore" + + [tool.ty.analysis] + respect-type-ignore-comments = false + "#, + ), + ( + "script.py", + r#" + # /// script + # dependencies = [] + # /// + + value: int = "not an int" + suppressed: int = "not an int" # type: ignore + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-assignment]: Object of type `Literal["not an int"]` is not assignable to `int` + --> script.py:6:8 + | + 6 | value: int = "not an int" + | --- ^^^^^^^^^^^^ Incompatible value of type `Literal["not an int"]` + | | + | Declared type + | + + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +#[test] +fn environment_options() -> anyhow::Result<()> { + // TODO: This is not yet supported, but we should support this. + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.12" + "#, + ), + ( + "script.py", + r#" + # /// script + # requires-python = ">=3.7" + # + # [tool.ty.environment] + # python-version = "3.7" + # /// + + import sys + from typing import reveal_type + + reveal_type(sys.version_info[:2] == (3, 12)) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> script.py:12:13 + | + 12 | reveal_type(sys.version_info[:2] == (3, 12)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal[True]` + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn inline_overrides_are_ignored() -> anyhow::Result<()> { + // TODO: Emit a diagnostic for options that are not allowed within scripts. + let case = CliTest::with_file( + "script.py", + r#" + # /// script + # [tool.ty.rules] + # unresolved-reference = "warn" + # + # [[tool.ty.overrides]] + # include = ["script.py"] + # + # [tool.ty.overrides.rules] + # unresolved-reference = "ignore" + # /// + + print(missing) + "#, + )?; + + assert_cmd_snapshot!(case.command(), @" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `missing` used when not defined + --> script.py:13:7 + | + 13 | print(missing) + | ^^^^^^^ + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn inline_terminal_settings_do_not_apply() -> anyhow::Result<()> { + // TODO: Either support (when calling `ty check