Compare commits

..

No commits in common. "main" and "0.14.6" have entirely different histories.
main ... 0.14.6

883 changed files with 16027 additions and 257060 deletions

View File

@ -7,6 +7,10 @@ serial = { max-threads = 1 }
filter = 'binary(file_watching)' filter = 'binary(file_watching)'
test-group = 'serial' test-group = 'serial'
[[profile.default.overrides]]
filter = 'binary(e2e)'
test-group = 'serial'
[profile.ci] [profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end # Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability). # of the run (for easy scrollability).

View File

@ -2,11 +2,12 @@
$schema: "https://docs.renovatebot.com/renovate-schema.json", $schema: "https://docs.renovatebot.com/renovate-schema.json",
dependencyDashboard: true, dependencyDashboard: true,
suppressNotifications: ["prEditedNotification"], suppressNotifications: ["prEditedNotification"],
extends: ["github>astral-sh/renovate-config"], extends: ["config:recommended"],
labels: ["internal"], labels: ["internal"],
schedule: ["before 4am on Monday"], schedule: ["before 4am on Monday"],
semanticCommits: "disabled", semanticCommits: "disabled",
separateMajorMinor: false, separateMajorMinor: false,
prHourlyLimit: 10,
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "pip_requirements", "npm"], enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "pip_requirements", "npm"],
cargo: { cargo: {
// See https://docs.renovatebot.com/configuration-options/#rangestrategy // See https://docs.renovatebot.com/configuration-options/#rangestrategy
@ -15,7 +16,7 @@
pep621: { pep621: {
// The default for this package manager is to only search for `pyproject.toml` files // The default for this package manager is to only search for `pyproject.toml` files
// found at the repository root: https://docs.renovatebot.com/modules/manager/pep621/#file-matching // found at the repository root: https://docs.renovatebot.com/modules/manager/pep621/#file-matching
managerFilePatterns: ["^(python|scripts)/.*pyproject\\.toml$"], fileMatch: ["^(python|scripts)/.*pyproject\\.toml$"],
}, },
pip_requirements: { pip_requirements: {
// The default for this package manager is to run on all requirements.txt files: // The default for this package manager is to run on all requirements.txt files:
@ -33,7 +34,7 @@
npm: { npm: {
// The default for this package manager is to only search for `package.json` files // The default for this package manager is to only search for `package.json` files
// found at the repository root: https://docs.renovatebot.com/modules/manager/npm/#file-matching // found at the repository root: https://docs.renovatebot.com/modules/manager/npm/#file-matching
managerFilePatterns: ["^playground/.*package\\.json$"], fileMatch: ["^playground/.*package\\.json$"],
}, },
"pre-commit": { "pre-commit": {
enabled: true, enabled: true,
@ -75,6 +76,14 @@
matchManagers: ["cargo"], matchManagers: ["cargo"],
enabled: false, enabled: false,
}, },
{
// `mkdocs-material` requires a manual update to keep the version in sync
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackageNames: ["mkdocs-material"],
enabled: false,
},
{ {
groupName: "pre-commit dependencies", groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"], matchManagers: ["pre-commit"],

View File

@ -43,7 +43,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"
@ -72,7 +72,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@ -114,7 +114,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64 architecture: arm64
@ -170,7 +170,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }} architecture: ${{ matrix.platform.arch }}
@ -223,7 +223,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@ -300,7 +300,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"
@ -365,7 +365,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@ -431,7 +431,7 @@ jobs:
with: with:
submodules: recursive submodules: recursive
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"

View File

@ -24,8 +24,6 @@ env:
PACKAGE_NAME: ruff PACKAGE_NAME: ruff
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.14"
NEXTEST_PROFILE: ci NEXTEST_PROFILE: ci
# Enable mdtests that require external dependencies
MDTEST_EXTERNAL: "1"
jobs: jobs:
determine_changes: determine_changes:
@ -232,7 +230,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -254,7 +252,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
shared-key: ruff-linux-debug shared-key: ruff-linux-debug
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
@ -263,15 +261,15 @@ jobs:
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest" - name: "Install cargo nextest"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-nextest tool: cargo-nextest
- name: "Install cargo insta" - name: "Install cargo insta"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-insta tool: cargo-insta
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
enable-cache: "true" enable-cache: "true"
- name: ty mdtests (GitHub annotations) - name: ty mdtests (GitHub annotations)
@ -286,10 +284,6 @@ jobs:
run: cargo insta test --all-features --unreferenced reject --test-runner nextest run: cargo insta test --all-features --unreferenced reject --test-runner nextest
- name: Dogfood ty on py-fuzzer - name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
- name: Dogfood ty on the scripts directory
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
- name: Dogfood ty on ty_benchmark
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark
# Check for broken links in the documentation. # Check for broken links in the documentation.
- run: cargo doc --all --no-deps - run: cargo doc --all --no-deps
env: env:
@ -298,7 +292,7 @@ jobs:
# sync, not just public items. Eventually we should do this for all # sync, not just public items. Eventually we should do this for all
# crates; for now add crates here as they are warning-clean to prevent # crates; for now add crates here as they are warning-clean to prevent
# regression. # regression.
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items
env: env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings"
@ -317,7 +311,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -325,11 +319,11 @@ jobs:
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest" - name: "Install cargo nextest"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-nextest tool: cargo-nextest
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
enable-cache: "true" enable-cache: "true"
- name: "Run tests" - name: "Run tests"
@ -352,17 +346,17 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install cargo nextest" - name: "Install cargo nextest"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-nextest tool: cargo-nextest
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
enable-cache: "true" enable-cache: "true"
- name: "Run tests" - name: "Run tests"
@ -380,7 +374,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -417,7 +411,7 @@ jobs:
with: with:
file: "Cargo.toml" file: "Cargo.toml"
field: "workspace.package.rust-version" field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -441,7 +435,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
workspaces: "fuzz -> target" workspaces: "fuzz -> target"
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
@ -450,7 +444,7 @@ jobs:
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo-binstall" - name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2 uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
- name: "Install cargo-fuzz" - name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets. # Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@ -468,8 +462,8 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
shared-key: ruff-linux-debug shared-key: ruff-linux-debug
save-if: false save-if: false
@ -500,10 +494,10 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup component add rustfmt run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is # Run all code generation scripts, and verify that the current output is
@ -538,7 +532,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }} ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true activate-environment: true
@ -549,7 +543,7 @@ jobs:
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
shared-key: ruff-linux-debug shared-key: ruff-linux-debug
save-if: false save-if: false
@ -644,8 +638,8 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -690,7 +684,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2 - uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
- run: cargo binstall --no-confirm cargo-shear - run: cargo binstall --no-confirm cargo-shear
- run: cargo shear - run: cargo shear
@ -703,8 +697,8 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -725,11 +719,11 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Prep README.md" - name: "Prep README.md"
@ -754,8 +748,8 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
@ -781,21 +775,32 @@ jobs:
name: "mkdocs" name: "mkdocs"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
python-version: 3.13 python-version: 3.13
activate-environment: true activate-environment: true
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt
- name: "Install dependencies" - name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: uv pip install -r docs/requirements.txt run: uv pip install -r docs/requirements.txt
- name: "Update README File" - name: "Update README File"
run: python scripts/transform_readme.py --target mkdocs run: python scripts/transform_readme.py --target mkdocs
@ -803,8 +808,12 @@ jobs:
run: python scripts/generate_mkdocs.py run: python scripts/generate_mkdocs.py
- name: "Check docs formatting" - name: "Check docs formatting"
run: python scripts/check_docs_formatted.py run: python scripts/check_docs_formatted.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs" - name: "Build docs"
run: mkdocs build --strict -f mkdocs.yml if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
check-formatter-instability-and-black-similarity: check-formatter-instability-and-black-similarity:
name: "formatter instabilities and black similarity" name: "formatter instabilities and black similarity"
@ -816,7 +825,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -844,7 +853,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
shared-key: ruff-linux-debug shared-key: ruff-linux-debug
save-if: false save-if: false
@ -862,7 +871,7 @@ jobs:
repository: "astral-sh/ruff-lsp" repository: "astral-sh/ruff-lsp"
path: ruff-lsp path: ruff-lsp
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
# installation fails on 3.13 and newer # installation fails on 3.13 and newer
python-version: "3.12" python-version: "3.12"
@ -895,7 +904,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
@ -905,7 +914,7 @@ jobs:
cache-dependency-path: playground/package-lock.json cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies" - name: "Install Node dependencies"
run: npm ci --ignore-scripts run: npm ci
working-directory: playground working-directory: playground
- name: "Build playgrounds" - name: "Build playgrounds"
run: npm run dev:wasm run: npm run dev:wasm
@ -929,25 +938,22 @@ jobs:
needs.determine_changes.outputs.linter == 'true' needs.determine_changes.outputs.linter == 'true'
) )
timeout-minutes: 20 timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
steps: steps:
- name: "Checkout Branch" - name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install codspeed" - name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-codspeed tool: cargo-codspeed
@ -955,10 +961,11 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks" - name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
with: with:
mode: simulation mode: instrumentation
run: cargo codspeed run run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-instrumented-ty: benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)" name: "benchmarks instrumented (ty)"
@ -971,25 +978,22 @@ jobs:
needs.determine_changes.outputs.ty == 'true' needs.determine_changes.outputs.ty == 'true'
) )
timeout-minutes: 20 timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
steps: steps:
- name: "Checkout Branch" - name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install codspeed" - name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-codspeed tool: cargo-codspeed
@ -997,10 +1001,11 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks" - name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
with: with:
mode: simulation mode: instrumentation
run: cargo codspeed run run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-walltime: benchmarks-walltime:
name: "benchmarks walltime (${{ matrix.benchmarks }})" name: "benchmarks walltime (${{ matrix.benchmarks }})"
@ -1008,9 +1013,6 @@ jobs:
needs: determine_changes needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }} if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20 timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy: strategy:
matrix: matrix:
benchmarks: benchmarks:
@ -1022,16 +1024,16 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install codspeed" - name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-codspeed tool: cargo-codspeed
@ -1039,7 +1041,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Run benchmarks" - name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
env: env:
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # 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 # appear to provide much useful insight for our walltime benchmarks right now
@ -1048,3 +1050,4 @@ jobs:
with: with:
mode: walltime mode: walltime
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}" run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -34,12 +34,12 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: Build ruff - name: Build ruff
# A debug build means the script runs slower once it gets started, # A debug build means the script runs slower once it gets started,
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI # but this is outweighed by the fact that a release build takes *much* longer to compile in CI

View File

@ -43,11 +43,10 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
shared-key: "mypy-primer"
workspaces: "ruff" workspaces: "ruff"
- name: Install Rust toolchain - name: Install Rust toolchain
@ -82,12 +81,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
workspaces: "ruff" workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain - name: Install Rust toolchain
run: rustup show run: rustup show
@ -107,54 +105,3 @@ jobs:
with: with:
name: mypy_primer_memory_diff name: mypy_primer_memory_diff
path: mypy_primer_memory.diff path: mypy_primer_memory.diff
# Runs mypy twice against the same ty version to catch any non-deterministic behavior (ideally).
# The job is disabled for now because there are some non-deterministic diagnostics.
mypy_primer_same_revision:
name: Run mypy_primer on same revision
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
timeout-minutes: 20
# TODO: Enable once we fixed the non-deterministic diagnostics
if: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain
run: rustup show
- name: Run determinism check
env:
BASE_REVISION: ${{ github.event.pull_request.head.sha }}
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer_determinism.diff
run: |
cd ruff
scripts/mypy_primer.sh
- name: Check for non-determinism
run: |
# Remove ANSI color codes for checking
sed -e 's/\x1b\[[0-9;]*m//g' mypy_primer_determinism.diff > mypy_primer_determinism_clean.diff
# Check if there are any differences (non-determinism)
if [ -s mypy_primer_determinism_clean.diff ]; then
echo "ERROR: Non-deterministic output detected!"
echo "The following differences were found when running ty twice on the same commit:"
cat mypy_primer_determinism_clean.diff
exit 1
else
echo "✓ Output is deterministic"
fi

View File

@ -20,13 +20,15 @@ on:
jobs: jobs:
mkdocs: mkdocs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
persist-credentials: true persist-credentials: true
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: 3.12 python-version: 3.12
@ -57,12 +59,23 @@ jobs:
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV" echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
echo "timestamp=$timestamp" >> "$GITHUB_ENV" echo "timestamp=$timestamp" >> "$GITHUB_ENV"
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: pip install -r docs/requirements-insiders.txt
- name: "Install dependencies" - name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: pip install -r docs/requirements.txt run: pip install -r docs/requirements.txt
- name: "Copy README File" - name: "Copy README File"
@ -70,8 +83,13 @@ jobs:
python scripts/transform_readme.py --target mkdocs python scripts/transform_readme.py --target mkdocs
python scripts/generate_mkdocs.py python scripts/generate_mkdocs.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs" - name: "Build docs"
run: mkdocs build --strict -f mkdocs.yml if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
- name: "Clone docs repo" - name: "Clone docs repo"
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs

View File

@ -37,7 +37,7 @@ jobs:
package-manager-cache: false package-manager-cache: false
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies" - name: "Install Node dependencies"
run: npm ci --ignore-scripts run: npm ci
working-directory: playground working-directory: playground
- name: "Run TypeScript checks" - name: "Run TypeScript checks"
run: npm run check run: npm run check

View File

@ -22,7 +22,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
pattern: wheels-* pattern: wheels-*

View File

@ -41,7 +41,7 @@ jobs:
package-manager-cache: false package-manager-cache: false
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies" - name: "Install Node dependencies"
run: npm ci --ignore-scripts run: npm ci
working-directory: playground working-directory: playground
- name: "Run TypeScript checks" - name: "Run TypeScript checks"
run: npm run check run: npm run check

View File

@ -60,7 +60,7 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@ -123,7 +123,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@ -174,7 +174,7 @@ jobs:
outputs: outputs:
val: ${{ steps.host.outputs.manifest }} val: ${{ steps.host.outputs.manifest }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@ -250,7 +250,7 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive

View File

@ -77,7 +77,7 @@ jobs:
run: | run: |
git config --global user.name typeshedbot git config --global user.name typeshedbot
git config --global user.email '<>' git config --global user.email '<>'
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Sync typeshed stubs - name: Sync typeshed stubs
run: | run: |
rm -rf "ruff/${VENDORED_TYPESHED}" rm -rf "ruff/${VENDORED_TYPESHED}"
@ -131,7 +131,7 @@ jobs:
with: with:
persist-credentials: true persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}} ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Setup git - name: Setup git
run: | run: |
git config --global user.name typeshedbot git config --global user.name typeshedbot
@ -170,7 +170,7 @@ jobs:
with: with:
persist-credentials: true persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}} ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Setup git - name: Setup git
run: | run: |
git config --global user.name typeshedbot git config --global user.name typeshedbot
@ -198,7 +198,7 @@ jobs:
run: | run: |
rm "${VENDORED_TYPESHED}/pyproject.toml" rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file" git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
if: ${{ success() }} if: ${{ success() }}
run: rustup show run: rustup show
@ -207,12 +207,12 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest" - name: "Install cargo nextest"
if: ${{ success() }} if: ${{ success() }}
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-nextest tool: cargo-nextest
- name: "Install cargo insta" - name: "Install cargo insta"
if: ${{ success() }} if: ${{ success() }}
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with: with:
tool: cargo-insta tool: cargo-insta
- name: Update snapshots - name: Update snapshots

View File

@ -33,11 +33,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
workspaces: "ruff" workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
@ -67,7 +67,7 @@ jobs:
cd .. cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5" uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \ ecosystem-analyzer \
--repository ruff \ --repository ruff \

View File

@ -29,11 +29,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with: with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
workspaces: "ruff" workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
@ -52,7 +52,7 @@ jobs:
cd .. cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5" uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \ ecosystem-analyzer \
--verbose \ --verbose \

View File

@ -45,7 +45,7 @@ jobs:
path: typing path: typing
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with: with:
workspaces: "ruff" workspaces: "ruff"

View File

@ -5,6 +5,5 @@
"rust-analyzer.check.command": "clippy", "rust-analyzer.check.command": "clippy",
"search.exclude": { "search.exclude": {
"**/*.snap": true "**/*.snap": true
}, }
"ty.diagnosticMode": "openFilesOnly"
} }

View File

@ -1,111 +1,5 @@
# Changelog # Changelog
## 0.14.9
Released on 2025-12-11.
### Preview features
- \[`ruff`\] New `RUF100` diagnostics for unused range suppressions ([#21783](https://github.com/astral-sh/ruff/pull/21783))
- \[`pylint`\] Detect subclasses of builtin exceptions (`PLW0133`) ([#21382](https://github.com/astral-sh/ruff/pull/21382))
### Bug fixes
- Fix comment placement in lambda parameters ([#21868](https://github.com/astral-sh/ruff/pull/21868))
- Skip over trivia tokens after re-lexing ([#21895](https://github.com/astral-sh/ruff/pull/21895))
- \[`flake8-bandit`\] Fix false positive when using non-standard `CSafeLoader` path (S506). ([#21830](https://github.com/astral-sh/ruff/pull/21830))
- \[`flake8-bugbear`\] Accept immutable slice default arguments (`B008`) ([#21823](https://github.com/astral-sh/ruff/pull/21823))
### Rule changes
- \[`pydocstyle`\] Suppress `D417` for parameters with `Unpack` annotations ([#21816](https://github.com/astral-sh/ruff/pull/21816))
### Performance
- Use `memchr` for computing line indexes ([#21838](https://github.com/astral-sh/ruff/pull/21838))
### Documentation
- Document `*.pyw` is included by default in preview ([#21885](https://github.com/astral-sh/ruff/pull/21885))
- Document range suppressions, reorganize suppression docs ([#21884](https://github.com/astral-sh/ruff/pull/21884))
- Update mkdocs-material to 9.7.0 (Insiders now free) ([#21797](https://github.com/astral-sh/ruff/pull/21797))
### Contributors
- [@Avasam](https://github.com/Avasam)
- [@MichaReiser](https://github.com/MichaReiser)
- [@charliermarsh](https://github.com/charliermarsh)
- [@amyreese](https://github.com/amyreese)
- [@phongddo](https://github.com/phongddo)
- [@prakhar1144](https://github.com/prakhar1144)
- [@mahiro72](https://github.com/mahiro72)
- [@ntBre](https://github.com/ntBre)
- [@LoicRiegel](https://github.com/LoicRiegel)
## 0.14.8
Released on 2025-12-04.
### Preview features
- \[`flake8-bugbear`\] Catch `yield` expressions within other statements (`B901`) ([#21200](https://github.com/astral-sh/ruff/pull/21200))
- \[`flake8-use-pathlib`\] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) ([#21440](https://github.com/astral-sh/ruff/pull/21440))
### Bug fixes
- Fix syntax error false positives for `await` outside functions ([#21763](https://github.com/astral-sh/ruff/pull/21763))
- \[`flake8-simplify`\] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) ([#21479](https://github.com/astral-sh/ruff/pull/21479))
### Documentation
- Suggest using `--output-file` option in GitLab integration ([#21706](https://github.com/astral-sh/ruff/pull/21706))
### Other changes
- [syntax-error] Default type parameter followed by non-default type parameter ([#21657](https://github.com/astral-sh/ruff/pull/21657))
### Contributors
- [@kieran-ryan](https://github.com/kieran-ryan)
- [@11happy](https://github.com/11happy)
- [@danparizher](https://github.com/danparizher)
- [@ntBre](https://github.com/ntBre)
## 0.14.7
Released on 2025-11-28.
### Preview features
- \[`flake8-bandit`\] Handle string literal bindings in suspicious-url-open-usage (`S310`) ([#21469](https://github.com/astral-sh/ruff/pull/21469))
- \[`pylint`\] Fix `PLR1708` false positives on nested functions ([#21177](https://github.com/astral-sh/ruff/pull/21177))
- \[`pylint`\] Fix suppression for empty dict without tuple key annotation (`PLE1141`) ([#21290](https://github.com/astral-sh/ruff/pull/21290))
- \[`ruff`\] Add rule `RUF066` to detect unnecessary class properties ([#21535](https://github.com/astral-sh/ruff/pull/21535))
- \[`ruff`\] Catch more dummy variable uses (`RUF052`) ([#19799](https://github.com/astral-sh/ruff/pull/19799))
### Bug fixes
- [server] Set severity for non-rule diagnostics ([#21559](https://github.com/astral-sh/ruff/pull/21559))
- \[`flake8-implicit-str-concat`\] Avoid invalid fix in (`ISC003`) ([#21517](https://github.com/astral-sh/ruff/pull/21517))
- \[`parser`\] Fix panic when parsing IPython escape command expressions ([#21480](https://github.com/astral-sh/ruff/pull/21480))
### CLI
- Show partial fixability indicator in statistics output ([#21513](https://github.com/astral-sh/ruff/pull/21513))
### Contributors
- [@mikeleppane](https://github.com/mikeleppane)
- [@senekor](https://github.com/senekor)
- [@ShaharNaveh](https://github.com/ShaharNaveh)
- [@JumboBear](https://github.com/JumboBear)
- [@prakhar1144](https://github.com/prakhar1144)
- [@tsvikas](https://github.com/tsvikas)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.14.6 ## 0.14.6
Released on 2025-11-21. Released on 2025-11-21.

View File

@ -331,6 +331,13 @@ you addressed them.
## MkDocs ## MkDocs
> [!NOTE]
>
> The documentation uses Material for MkDocs Insiders, which is closed-source software.
> This means only members of the Astral organization can preview the documentation exactly as it
> will appear in production.
> Outside contributors can still preview the documentation, but there will be some differences. Consult [the Material for MkDocs documentation](https://squidfunk.github.io/mkdocs-material/insiders/benefits/#features) for which features are exclusively available in the insiders version.
To preview any changes to the documentation locally: To preview any changes to the documentation locally:
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install). 1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
@ -344,7 +351,11 @@ To preview any changes to the documentation locally:
1. Run the development server with: 1. Run the development server with:
```shell ```shell
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.yml # For contributors.
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml
``` ```
The documentation should then be available locally at The documentation should then be available locally at

134
Cargo.lock generated
View File

@ -254,21 +254,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -457,9 +442,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.53" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -467,9 +452,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.53" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -959,18 +944,6 @@ dependencies = [
"parking_lot_core", "parking_lot_core",
] ]
[[package]]
name = "datatest-stable"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a867d7322eb69cf3a68a5426387a25b45cb3b9c5ee41023ee6cea92e2afadd82"
dependencies = [
"camino",
"fancy-regex",
"libtest-mimic 0.8.1",
"walkdir",
]
[[package]] [[package]]
name = "derive-where" name = "derive-where"
version = "1.6.0" version = "1.6.0"
@ -1165,17 +1138,6 @@ dependencies = [
"windows-sys 0.61.0", "windows-sys 0.61.0",
] ]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -1276,9 +1238,9 @@ dependencies = [
[[package]] [[package]]
name = "get-size-derive2" name = "get-size-derive2"
version = "0.7.3" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd"
dependencies = [ dependencies = [
"attribute-derive", "attribute-derive",
"quote", "quote",
@ -1287,15 +1249,14 @@ dependencies = [
[[package]] [[package]]
name = "get-size2" name = "get-size2"
version = "0.7.3" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb" checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
dependencies = [ dependencies = [
"compact_str", "compact_str",
"get-size-derive2", "get-size-derive2",
"hashbrown 0.16.1", "hashbrown 0.16.0",
"indexmap", "indexmap",
"ordermap",
"smallvec", "smallvec",
] ]
@ -1392,9 +1353,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.1" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
] ]
@ -1603,12 +1564,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.1" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.16.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@ -1663,6 +1624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
dependencies = [ dependencies = [
"console 0.15.11", "console 0.15.11",
"globset",
"once_cell", "once_cell",
"pest", "pest",
"pest_derive", "pest_derive",
@ -1670,6 +1632,7 @@ dependencies = [
"ron", "ron",
"serde", "serde",
"similar", "similar",
"walkdir",
] ]
[[package]] [[package]]
@ -1955,18 +1918,6 @@ dependencies = [
"threadpool", "threadpool",
] ]
[[package]]
name = "libtest-mimic"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33"
dependencies = [
"anstream",
"anstyle",
"clap",
"escape8259",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@ -2282,9 +2233,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordermap" name = "ordermap"
version = "1.0.0" version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed637741ced8fb240855d22a2b4f208dab7a06bcce73380162e5253000c16758" checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2908,7 +2859,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.9" version = "0.14.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argfile", "argfile",
@ -3166,18 +3117,17 @@ dependencies = [
[[package]] [[package]]
name = "ruff_linter" name = "ruff_linter"
version = "0.14.9" version = "0.14.6"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow", "anyhow",
"bitflags 2.10.0", "bitflags 2.10.0",
"clap", "clap",
"colored 3.0.0", "colored 3.0.0",
"compact_str",
"fern", "fern",
"glob", "glob",
"globset", "globset",
"hashbrown 0.16.1", "hashbrown 0.16.0",
"imperative", "imperative",
"insta", "insta",
"is-macro", "is-macro",
@ -3326,7 +3276,6 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"countme", "countme",
"datatest-stable",
"insta", "insta",
"itertools 0.14.0", "itertools 0.14.0",
"memchr", "memchr",
@ -3396,10 +3345,8 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bstr", "bstr",
"compact_str", "compact_str",
"datatest-stable",
"get-size2", "get-size2",
"insta", "insta",
"itertools 0.14.0",
"memchr", "memchr",
"ruff_annotate_snippets", "ruff_annotate_snippets",
"ruff_python_ast", "ruff_python_ast",
@ -3525,7 +3472,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff_wasm" name = "ruff_wasm"
version = "0.14.9" version = "0.14.6"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
@ -3641,7 +3588,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "salsa" name = "salsa"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
dependencies = [ dependencies = [
"boxcar", "boxcar",
"compact_str", "compact_str",
@ -3652,7 +3599,6 @@ dependencies = [
"indexmap", "indexmap",
"intrusive-collections", "intrusive-collections",
"inventory", "inventory",
"ordermap",
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
"rustc-hash", "rustc-hash",
@ -3666,12 +3612,12 @@ dependencies = [
[[package]] [[package]]
name = "salsa-macro-rules" name = "salsa-macro-rules"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
[[package]] [[package]]
name = "salsa-macros" name = "salsa-macros"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3989,9 +3935,9 @@ checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.111" version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4270,9 +4216,9 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.43" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@ -4282,9 +4228,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4293,9 +4239,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.35" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@ -4337,9 +4283,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.22" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [ dependencies = [
"chrono", "chrono",
"matchers", "matchers",
@ -4361,7 +4307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396" checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396"
dependencies = [ dependencies = [
"ignore", "ignore",
"libtest-mimic 0.7.3", "libtest-mimic",
"snapbox", "snapbox",
] ]
@ -4390,7 +4336,6 @@ dependencies = [
"ruff_python_trivia", "ruff_python_trivia",
"salsa", "salsa",
"tempfile", "tempfile",
"tikv-jemallocator",
"toml", "toml",
"tracing", "tracing",
"tracing-flame", "tracing-flame",
@ -4517,7 +4462,7 @@ dependencies = [
"drop_bomb", "drop_bomb",
"get-size2", "get-size2",
"glob", "glob",
"hashbrown 0.16.1", "hashbrown 0.16.0",
"indexmap", "indexmap",
"indoc", "indoc",
"insta", "insta",
@ -4529,7 +4474,6 @@ dependencies = [
"quickcheck_macros", "quickcheck_macros",
"ruff_annotate_snippets", "ruff_annotate_snippets",
"ruff_db", "ruff_db",
"ruff_diagnostics",
"ruff_index", "ruff_index",
"ruff_macros", "ruff_macros",
"ruff_memory_usage", "ruff_memory_usage",
@ -4575,7 +4519,6 @@ dependencies = [
"lsp-types", "lsp-types",
"regex", "regex",
"ruff_db", "ruff_db",
"ruff_diagnostics",
"ruff_macros", "ruff_macros",
"ruff_notebook", "ruff_notebook",
"ruff_python_ast", "ruff_python_ast",
@ -4611,13 +4554,11 @@ dependencies = [
"anyhow", "anyhow",
"camino", "camino",
"colored 3.0.0", "colored 3.0.0",
"dunce",
"insta", "insta",
"memchr", "memchr",
"path-slash", "path-slash",
"regex", "regex",
"ruff_db", "ruff_db",
"ruff_diagnostics",
"ruff_index", "ruff_index",
"ruff_notebook", "ruff_notebook",
"ruff_python_ast", "ruff_python_ast",
@ -4659,7 +4600,6 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"ruff_db", "ruff_db",
"ruff_diagnostics",
"ruff_notebook", "ruff_notebook",
"ruff_python_formatter", "ruff_python_formatter",
"ruff_source_file", "ruff_source_file",

View File

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package] [workspace.package]
# Please update rustfmt.toml when bumping the Rust edition # Please update rustfmt.toml when bumping the Rust edition
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.89"
homepage = "https://docs.astral.sh/ruff" homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff" documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff" repository = "https://github.com/astral-sh/ruff"
@ -81,7 +81,6 @@ compact_str = "0.9.0"
criterion = { version = "0.7.0", default-features = false } criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" } crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" } dashmap = { version = "6.0.1" }
datatest-stable = { version = "0.3.3" }
dir-test = { version = "0.4.0" } dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" } dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" } drop_bomb = { version = "0.1.5" }
@ -89,7 +88,7 @@ etcetera = { version = "0.11.0" }
fern = { version = "0.7.0" } fern = { version = "0.7.0" }
filetime = { version = "0.2.23" } filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" } getrandom = { version = "0.3.1" }
get-size2 = { version = "0.7.3", features = [ get-size2 = { version = "0.7.0", features = [
"derive", "derive",
"smallvec", "smallvec",
"hashbrown", "hashbrown",
@ -130,7 +129,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" } mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" } natord = { version = "1.0.9" }
notify = { version = "8.0.0" } notify = { version = "8.0.0" }
ordermap = { version = "1.0.0" } ordermap = { version = "0.5.0" }
path-absolutize = { version = "3.1.1" } path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" } path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
@ -147,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" } rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" } rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [ salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
"compact_str", "compact_str",
"macros", "macros",
"salsa_unstable", "salsa_unstable",
@ -273,12 +272,6 @@ large_stack_arrays = "allow"
lto = "fat" lto = "fat"
codegen-units = 16 codegen-units = 16
# Profile to build a minimally sized binary for ruff/ty
[profile.minimal-size]
inherits = "release"
opt-level = "z"
codegen-units = 1
# Some crates don't change as much but benefit more from # Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively # more expensive optimization passes, so we selectively
# decrease codegen-units in some cases. # decrease codegen-units in some cases.

View File

@ -57,11 +57,8 @@ Ruff is extremely actively developed and used in major open-source projects like
...and [many more](#whos-using-ruff). ...and [many more](#whos-using-ruff).
Ruff is backed by [Astral](https://astral.sh), the creators of Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
[uv](https://github.com/astral-sh/uv) and [ty](https://github.com/astral-sh/ty). or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), or the
original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
## Testimonials ## Testimonials
@ -150,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version. # For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.9/install.sh | sh curl -LsSf https://astral.sh/ruff/0.14.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.9/install.ps1 | iex" powershell -c "irm https://astral.sh/ruff/0.14.6/install.ps1 | iex"
``` ```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@ -184,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.9 rev: v0.14.6
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff-check - id: ruff-check

View File

@ -4,7 +4,6 @@ extend-exclude = [
"crates/ty_vendored/vendor/**/*", "crates/ty_vendored/vendor/**/*",
"**/resources/**/*", "**/resources/**/*",
"**/snapshots/**/*", "**/snapshots/**/*",
"crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs",
# Completion tests tend to have a lot of incomplete # Completion tests tend to have a lot of incomplete
# words naturally. It's annoying to have to make all # words naturally. It's annoying to have to make all
# of them actually words. So just ignore typos here. # of them actually words. So just ignore typos here.

View File

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

View File

@ -10,7 +10,7 @@ use anyhow::bail;
use clap::builder::Styles; use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects}; use clap::builder::styling::{AnsiColor, Effects};
use clap::builder::{TypedValueParser, ValueParserFactory}; use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand, command};
use colored::Colorize; use colored::Colorize;
use itertools::Itertools; use itertools::Itertools;
use path_absolutize::path_dedot; use path_absolutize::path_dedot;

View File

@ -9,7 +9,7 @@ use std::sync::mpsc::channel;
use anyhow::Result; use anyhow::Result;
use clap::CommandFactory; use clap::CommandFactory;
use colored::Colorize; use colored::Colorize;
use log::error; use log::{error, warn};
use notify::{RecursiveMode, Watcher, recommended_watcher}; use notify::{RecursiveMode, Watcher, recommended_watcher};
use args::{GlobalConfigArgs, ServerCommand}; use args::{GlobalConfigArgs, ServerCommand};

View File

@ -34,21 +34,9 @@ struct ExpandedStatistics<'a> {
code: Option<&'a SecondaryCode>, code: Option<&'a SecondaryCode>,
name: &'static str, name: &'static str,
count: usize, count: usize,
#[serde(rename = "fixable")] fixable: bool,
all_fixable: bool,
fixable_count: usize,
} }
impl ExpandedStatistics<'_> {
fn any_fixable(&self) -> bool {
self.fixable_count > 0
}
}
/// Accumulator type for grouping diagnostics by code.
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
pub(crate) struct Printer { pub(crate) struct Printer {
format: OutputFormat, format: OutputFormat,
log_level: LogLevel, log_level: LogLevel,
@ -145,7 +133,7 @@ impl Printer {
if fixables.applicable > 0 { if fixables.applicable > 0 {
writeln!( writeln!(
writer, writer,
"{fix_prefix} {} fixable with the `--fix` option.", "{fix_prefix} {} fixable with the --fix option.",
fixables.applicable fixables.applicable
)?; )?;
} }
@ -268,41 +256,35 @@ impl Printer {
diagnostics: &Diagnostics, diagnostics: &Diagnostics,
writer: &mut dyn Write, writer: &mut dyn Write,
) -> Result<()> { ) -> Result<()> {
let required_applicability = self.unsafe_fixes.required_applicability();
let statistics: Vec<ExpandedStatistics> = diagnostics let statistics: Vec<ExpandedStatistics> = diagnostics
.inner .inner
.iter() .iter()
.sorted_by_key(|diagnostic| diagnostic.secondary_code()) .map(|message| (message.secondary_code(), message))
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| { .sorted_by_key(|(code, message)| (*code, message.fixable()))
let is_fixable = diagnostic .fold(
.fix() vec![],
.is_some_and(|fix| fix.applies(required_applicability)); |mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
let code = diagnostic.secondary_code(); if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() { *count += 1;
if *prev_code == code { return acc;
*count += 1;
if is_fixable {
*fixable_count += 1;
} }
return acc;
} }
} acc.push(((code, message), 1));
acc.push((code, diagnostic, 1, usize::from(is_fixable))); acc
acc
})
.iter()
.map(
|&(code, message, count, fixable_count)| ExpandedStatistics {
code,
name: message.name(),
count,
// Backward compatibility: `fixable` is true only when all violations are fixable.
// See: https://github.com/astral-sh/ruff/pull/21513
all_fixable: fixable_count == count,
fixable_count,
}, },
) )
.iter()
.map(|&((code, message), count)| ExpandedStatistics {
code,
name: message.name(),
count,
fixable: if let Some(fix) = message.fix() {
fix.applies(self.unsafe_fixes.required_applicability())
} else {
false
},
})
.sorted_by_key(|statistic| Reverse(statistic.count)) .sorted_by_key(|statistic| Reverse(statistic.count))
.collect(); .collect();
@ -326,14 +308,13 @@ impl Printer {
.map(|statistic| statistic.code.map_or(0, |s| s.len())) .map(|statistic| statistic.code.map_or(0, |s| s.len()))
.max() .max()
.unwrap(); .unwrap();
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable); let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
let all_fixable = format!("[{}] ", "*".cyan()); let fixable = format!("[{}] ", "*".cyan());
let partially_fixable = format!("[{}] ", "-".cyan());
let unfixable = "[ ] "; let unfixable = "[ ] ";
// By default, we mimic Flake8's `--statistics` format. // By default, we mimic Flake8's `--statistics` format.
for statistic in &statistics { for statistic in statistics {
writeln!( writeln!(
writer, writer,
"{:>count_width$}\t{:<code_width$}\t{}{}", "{:>count_width$}\t{:<code_width$}\t{}{}",
@ -345,10 +326,8 @@ impl Printer {
.red() .red()
.bold(), .bold(),
if any_fixable { if any_fixable {
if statistic.all_fixable { if statistic.fixable {
&all_fixable &fixable
} else if statistic.any_fixable() {
&partially_fixable
} else { } else {
unfixable unfixable
} }

View File

@ -1440,78 +1440,6 @@ def function():
Ok(()) Ok(())
} }
#[test]
fn ignore_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
import os # noqa: F401
# ruff: disable[F401]
import sys
"#,
)?;
// without --ignore-noqa
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py"),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--preview"]),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
// with --ignore-noqa --preview
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--ignore-noqa", "--preview"]),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:2:8: F401 [*] `os` imported but unused
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
");
Ok(())
}
#[test] #[test]
fn add_noqa() -> Result<()> { fn add_noqa() -> Result<()> {
let fixture = CliTest::new()?; let fixture = CliTest::new()?;
@ -1704,100 +1632,6 @@ def unused(x): # noqa: ANN001, ARG001, D103
Ok(()) Ok(())
} }
#[test]
fn add_noqa_existing_file_level_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: noqa F401
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: noqa F401
import os
");
Ok(())
}
#[test]
fn add_noqa_existing_range_suppression() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: disable[F401]
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: disable[F401]
import os
");
Ok(())
}
#[test] #[test]
fn add_noqa_multiline_comment() -> Result<()> { fn add_noqa_multiline_comment() -> Result<()> {
let fixture = CliTest::new()?; let fixture = CliTest::new()?;

View File

@ -1043,7 +1043,7 @@ def mvce(keys, values):
----- stdout ----- ----- stdout -----
1 C416 [*] unnecessary-comprehension 1 C416 [*] unnecessary-comprehension
Found 1 error. Found 1 error.
[*] 1 fixable with the `--fix` option. [*] 1 fixable with the --fix option.
----- stderr ----- ----- stderr -----
"); ");
@ -1073,8 +1073,7 @@ def mvce(keys, values):
"code": "C416", "code": "C416",
"name": "unnecessary-comprehension", "name": "unnecessary-comprehension",
"count": 1, "count": 1,
"fixable": false, "fixable": false
"fixable_count": 0
} }
] ]
@ -1107,8 +1106,7 @@ def mvce(keys, values):
"code": "C416", "code": "C416",
"name": "unnecessary-comprehension", "name": "unnecessary-comprehension",
"count": 1, "count": 1,
"fixable": true, "fixable": true
"fixable_count": 1
} }
] ]
@ -1116,54 +1114,6 @@ def mvce(keys, values):
"#); "#);
} }
#[test]
fn show_statistics_json_partial_fix() {
let mut cmd = RuffCheck::default()
.args([
"--select",
"UP035",
"--statistics",
"--output-format",
"json",
])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "UP035",
"name": "deprecated-import",
"count": 2,
"fixable": false,
"fixable_count": 1
}
]
----- stderr -----
"#);
}
#[test]
fn show_statistics_partial_fix() {
let mut cmd = RuffCheck::default()
.args(["--select", "UP035", "--statistics"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r"
success: false
exit_code: 1
----- stdout -----
2 UP035 [-] deprecated-import
Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
}
#[test] #[test]
fn show_statistics_syntax_errors() { fn show_statistics_syntax_errors() {
let mut cmd = RuffCheck::default() let mut cmd = RuffCheck::default()
@ -1860,7 +1810,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
--> -:1:1 --> -:1:1
Found 2 errors. Found 2 errors.
[*] 1 fixable with the `--fix` option. [*] 1 fixable with the --fix option.
----- stderr ----- ----- stderr -----
"); ");
@ -1903,7 +1853,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
--> -:1:1 --> -:1:1
Found 2 errors. Found 2 errors.
[*] 2 fixable with the `--fix` option. [*] 2 fixable with the --fix option.
----- stderr ----- ----- stderr -----
"); ");

View File

@ -59,6 +59,8 @@ divan = { workspace = true, optional = true }
anyhow = { workspace = true } anyhow = { workspace = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true } codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
criterion = { workspace = true, default-features = false, optional = true } criterion = { workspace = true, default-features = false, optional = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
@ -86,7 +88,3 @@ mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies] [target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true } tikv-jemallocator = { workspace = true }
[dev-dependencies]
rustc-hash = { workspace = true }
rayon = { workspace = true }

View File

@ -6,8 +6,7 @@ use criterion::{
use ruff_benchmark::{ use ruff_benchmark::{
LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN,
}; };
use ruff_python_ast::token::TokenKind; use ruff_python_parser::{Mode, TokenKind, lexer};
use ruff_python_parser::{Mode, lexer};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[global_allocator] #[global_allocator]

View File

@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17", max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310, python_version: PythonVersion::PY310,
}, },
1070, 600,
); );
static FREQTRADE: Benchmark = Benchmark::new( static FREQTRADE: Benchmark = Benchmark::new(
@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17", max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312, python_version: PythonVersion::PY312,
}, },
13100, 13000,
); );
static TANJUN: Benchmark = Benchmark::new( static TANJUN: Benchmark = Benchmark::new(
@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09", max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311, python_version: PythonVersion::PY311,
}, },
1100, 900,
); );
#[track_caller] #[track_caller]

View File

@ -166,8 +166,28 @@ impl Diagnostic {
/// Returns the primary message for this diagnostic. /// Returns the primary message for this diagnostic.
/// ///
/// A diagnostic always has a message, but it may be empty. /// A diagnostic always has a message, but it may be empty.
///
/// NOTE: At present, this routine will return the first primary
/// annotation's message as the primary message when the main diagnostic
/// message is empty. This is meant to facilitate an incremental migration
/// in ty over to the new diagnostic data model. (The old data model
/// didn't distinguish between messages on the entire diagnostic and
/// messages attached to a particular span.)
pub fn primary_message(&self) -> &str { pub fn primary_message(&self) -> &str {
self.inner.message.as_str() if !self.inner.message.as_str().is_empty() {
return self.inner.message.as_str();
}
// FIXME: As a special case, while we're migrating ty
// to the new diagnostic data model, we'll look for a primary
// message from the primary annotation. This is because most
// ty diagnostics are created with an empty diagnostic
// message and instead attach the message to the annotation.
// Fixing this will require touching basically every diagnostic
// in ty, so we do it this way for now to match the old
// semantics. ---AG
self.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default()
} }
/// Introspects this diagnostic and returns what kind of "primary" message /// Introspects this diagnostic and returns what kind of "primary" message
@ -179,6 +199,18 @@ impl Diagnostic {
/// contains *essential* information or context for understanding the /// contains *essential* information or context for understanding the
/// diagnostic. /// diagnostic.
/// ///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most /// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what /// cases, just converting it to a string (or printing it) will do what
/// you want. /// you want.
@ -192,10 +224,11 @@ impl Diagnostic {
.primary_annotation() .primary_annotation()
.and_then(|ann| ann.get_message()) .and_then(|ann| ann.get_message())
.unwrap_or_default(); .unwrap_or_default();
if annotation.is_empty() { match (main.is_empty(), annotation.is_empty()) {
ConciseMessage::MainDiagnostic(main) (false, true) => ConciseMessage::MainDiagnostic(main),
} else { (true, false) => ConciseMessage::PrimaryAnnotation(annotation),
ConciseMessage::Both { main, annotation } (false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
} }
} }
@ -321,13 +354,6 @@ impl Diagnostic {
Arc::make_mut(&mut self.inner).fix = Some(fix); Arc::make_mut(&mut self.inner).fix = Some(fix);
} }
/// If `fix` is `Some`, set the fix for this diagnostic.
pub fn set_optional_fix(&mut self, fix: Option<Fix>) {
if let Some(fix) = fix {
self.set_fix(fix);
}
}
/// Remove the fix for this diagnostic. /// Remove the fix for this diagnostic.
pub fn remove_fix(&mut self) { pub fn remove_fix(&mut self) {
Arc::make_mut(&mut self.inner).fix = None; Arc::make_mut(&mut self.inner).fix = None;
@ -660,6 +686,18 @@ impl SubDiagnostic {
/// contains *essential* information or context for understanding the /// contains *essential* information or context for understanding the
/// diagnostic. /// diagnostic.
/// ///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most /// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what /// cases, just converting it to a string (or printing it) will do what
/// you want. /// you want.
@ -669,10 +707,11 @@ impl SubDiagnostic {
.primary_annotation() .primary_annotation()
.and_then(|ann| ann.get_message()) .and_then(|ann| ann.get_message())
.unwrap_or_default(); .unwrap_or_default();
if annotation.is_empty() { match (main.is_empty(), annotation.is_empty()) {
ConciseMessage::MainDiagnostic(main) (false, true) => ConciseMessage::MainDiagnostic(main),
} else { (true, false) => ConciseMessage::PrimaryAnnotation(annotation),
ConciseMessage::Both { main, annotation } (false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
} }
} }
} }
@ -842,10 +881,6 @@ impl Annotation {
pub fn hide_snippet(&mut self, yes: bool) { pub fn hide_snippet(&mut self, yes: bool) {
self.hide_snippet = yes; self.hide_snippet = yes;
} }
pub fn is_primary(&self) -> bool {
self.is_primary
}
} }
/// Tags that can be associated with an annotation. /// Tags that can be associated with an annotation.
@ -1466,10 +1501,28 @@ pub enum DiagnosticFormat {
pub enum ConciseMessage<'a> { pub enum ConciseMessage<'a> {
/// A diagnostic contains a non-empty main message and an empty /// A diagnostic contains a non-empty main message and an empty
/// primary annotation message. /// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
MainDiagnostic(&'a str), MainDiagnostic(&'a str),
/// A diagnostic contains an empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "old" data model.
PrimaryAnnotation(&'a str),
/// A diagnostic contains a non-empty main message and a non-empty /// A diagnostic contains a non-empty main message and a non-empty
/// primary annotation message. /// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
Both { main: &'a str, annotation: &'a str }, Both { main: &'a str, annotation: &'a str },
/// A diagnostic contains an empty main message and an empty
/// primary annotation message.
///
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided. /// A custom concise message has been provided.
Custom(&'a str), Custom(&'a str),
} }
@ -1480,9 +1533,13 @@ impl std::fmt::Display for ConciseMessage<'_> {
ConciseMessage::MainDiagnostic(main) => { ConciseMessage::MainDiagnostic(main) => {
write!(f, "{main}") write!(f, "{main}")
} }
ConciseMessage::PrimaryAnnotation(annotation) => {
write!(f, "{annotation}")
}
ConciseMessage::Both { main, annotation } => { ConciseMessage::Both { main, annotation } => {
write!(f, "{main}: {annotation}") write!(f, "{main}: {annotation}")
} }
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => { ConciseMessage::Custom(message) => {
write!(f, "{message}") write!(f, "{message}")
} }

View File

@ -21,11 +21,7 @@ use crate::source::source_text;
/// reflected in the changed AST offsets. /// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires /// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged. /// for determining if a query result is unchanged.
/// #[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)]
/// The LRU capacity of 200 was picked without any empirical evidence that it's optimal,
/// instead it's a wild guess that it should be unlikely that incremental changes involve
/// more than 200 modules. Parsed ASTs within the same revision are never evicted by Salsa.
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size, lru=200)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered(); let _span = tracing::trace_span!("parsed_module", ?file).entered();
@ -96,9 +92,14 @@ impl ParsedModule {
self.inner.store(None); self.inner.store(None);
} }
/// Returns the file to which this module belongs. /// Returns the pointer address of this [`ParsedModule`].
pub fn file(&self) -> File { ///
self.file /// The pointer uniquely identifies the module within the current Salsa revision,
/// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected.
pub fn addr(&self) -> usize {
// Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner
// `Arc` within the `ArcSwap` may change.
Arc::as_ptr(&self.inner).addr()
} }
} }

View File

@ -667,13 +667,6 @@ impl Deref for SystemPathBuf {
} }
} }
impl AsRef<Path> for SystemPathBuf {
#[inline]
fn as_ref(&self) -> &Path {
self.0.as_std_path()
}
}
impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf { impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf {
fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
let mut buf = SystemPathBuf::new(); let mut buf = SystemPathBuf::new();

View File

@ -144,8 +144,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n'); output.push('\n');
if let Some(deprecated) = &field.deprecated { if let Some(deprecated) = &field.deprecated {
output.push_str("!!! warning \"Deprecated\"\n"); output.push_str("> [!WARN] \"Deprecated\"\n");
output.push_str(" This option has been deprecated"); output.push_str("> This option has been deprecated");
if let Some(since) = deprecated.since { if let Some(since) = deprecated.since {
write!(output, " in {since}").unwrap(); write!(output, " in {since}").unwrap();
@ -166,9 +166,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n'); output.push('\n');
let _ = writeln!(output, "**Type**: `{}`", field.value_type); let _ = writeln!(output, "**Type**: `{}`", field.value_type);
output.push('\n'); output.push('\n');
output.push_str("**Example usage**:\n\n"); output.push_str("**Example usage** (`pyproject.toml`):\n\n");
output.push_str(&format_example( output.push_str(&format_example(
"pyproject.toml",
&format_header( &format_header(
field.scope, field.scope,
field.example, field.example,
@ -180,11 +179,11 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n'); output.push('\n');
} }
fn format_example(title: &str, header: &str, content: &str) -> String { fn format_example(header: &str, content: &str) -> String {
if header.is_empty() { if header.is_empty() {
format!("```toml title=\"{title}\"\n{content}\n```\n",) format!("```toml\n{content}\n```\n",)
} else { } else {
format!("```toml title=\"{title}\"\n{header}\n{content}\n```\n",) format!("```toml\n{header}\n{content}\n```\n",)
} }
} }

View File

@ -39,7 +39,7 @@ impl Edit {
/// Creates an edit that replaces the content in `range` with `content`. /// Creates an edit that replaces the content in `range` with `content`.
pub fn range_replacement(content: String, range: TextRange) -> Self { pub fn range_replacement(content: String, range: TextRange) -> Self {
debug_assert!(!content.is_empty(), "Prefer `Edit::deletion`"); debug_assert!(!content.is_empty(), "Prefer `Fix::deletion`");
Self { Self {
content: Some(Box::from(content)), content: Some(Box::from(content)),

View File

@ -149,10 +149,6 @@ impl Fix {
&self.edits &self.edits
} }
pub fn into_edits(self) -> Vec<Edit> {
self.edits
}
/// Return the [`Applicability`] of the [`Fix`]. /// Return the [`Applicability`] of the [`Fix`].
pub fn applicability(&self) -> Applicability { pub fn applicability(&self) -> Applicability {
self.applicability self.applicability

View File

@ -337,7 +337,7 @@ macro_rules! best_fitting {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::prelude::*; use crate::prelude::*;
use crate::{FormatState, SimpleFormatOptions, VecBuffer}; use crate::{FormatState, SimpleFormatOptions, VecBuffer, write};
struct TestFormat; struct TestFormat;
@ -385,8 +385,8 @@ mod tests {
#[test] #[test]
fn best_fitting_variants_print_as_lists() { fn best_fitting_variants_print_as_lists() {
use crate::Formatted;
use crate::prelude::*; use crate::prelude::*;
use crate::{Formatted, format, format_args};
// The second variant below should be selected when printing at a width of 30 // The second variant below should be selected when printing at a width of 30
let formatted_best_fitting = format!( let formatted_best_fitting = format!(

View File

@ -49,7 +49,7 @@ impl ModuleImports {
// Resolve the imports. // Resolve the imports.
let mut resolved_imports = ModuleImports::default(); let mut resolved_imports = ModuleImports::default();
for import in imports { for import in imports {
for resolved in Resolver::new(db, path).resolve(import) { for resolved in Resolver::new(db).resolve(import) {
if let Some(path) = resolved.as_system_path() { if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf()); resolved_imports.insert(path.to_path_buf());
} }

View File

@ -1,9 +1,5 @@
use ruff_db::files::{File, FilePath, system_path_to_file}; use ruff_db::files::FilePath;
use ruff_db::system::SystemPath; use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use ty_python_semantic::{
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident,
};
use crate::ModuleDb; use crate::ModuleDb;
use crate::collector::CollectedImport; use crate::collector::CollectedImport;
@ -11,15 +7,12 @@ use crate::collector::CollectedImport;
/// Collect all imports for a given Python file. /// Collect all imports for a given Python file.
pub(crate) struct Resolver<'a> { pub(crate) struct Resolver<'a> {
db: &'a ModuleDb, db: &'a ModuleDb,
file: Option<File>,
} }
impl<'a> Resolver<'a> { impl<'a> Resolver<'a> {
/// Initialize a [`Resolver`] with a given [`ModuleDb`]. /// Initialize a [`Resolver`] with a given [`ModuleDb`].
pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self { pub(crate) fn new(db: &'a ModuleDb) -> Self {
// If we know the importing file we can potentially resolve more imports Self { db }
let file = system_path_to_file(db, path).ok();
Self { db, file }
} }
/// Resolve the [`CollectedImport`] into a [`FilePath`]. /// Resolve the [`CollectedImport`] into a [`FilePath`].
@ -77,21 +70,13 @@ impl<'a> Resolver<'a> {
/// Resolves a module name to a module. /// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = if let Some(file) = self.file { let module = resolve_module(self.db, module_name)?;
resolve_module(self.db, file, module_name)?
} else {
resolve_module_confident(self.db, module_name)?
};
Some(module.file(self.db)?.path(self.db)) Some(module.file(self.db)?.path(self.db))
} }
/// Resolves a module name to a module (stubs not allowed). /// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = if let Some(file) = self.file { let module = resolve_real_module(self.db, module_name)?;
resolve_real_module(self.db, file, module_name)?
} else {
resolve_real_module_confident(self.db, module_name)?
};
Some(module.file(self.db)?.path(self.db)) Some(module.file(self.db)?.path(self.db))
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ruff_linter" name = "ruff_linter"
version = "0.14.9" version = "0.14.6"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@ -35,7 +35,6 @@ anyhow = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true } clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true } colored = { workspace = true }
compact_str = { workspace = true }
fern = { workspace = true } fern = { workspace = true }
glob = { workspace = true } glob = { workspace = true }
globset = { workspace = true } globset = { workspace = true }

View File

@ -45,22 +45,3 @@ urllib.request.urlopen(urllib.request.Request(url))
# https://github.com/astral-sh/ruff/issues/15522 # https://github.com/astral-sh/ruff/issues/15522
map(urllib.request.urlopen, []) map(urllib.request.urlopen, [])
foo = urllib.request.urlopen foo = urllib.request.urlopen
# https://github.com/astral-sh/ruff/issues/21462
path = "https://example.com/data.csv"
urllib.request.urlretrieve(path, "data.csv")
url = "https://example.com/api"
urllib.request.Request(url)
# Test resolved f-strings and concatenated string literals
fstring_url = f"https://example.com/data.csv"
urllib.request.urlopen(fstring_url)
urllib.request.Request(fstring_url)
concatenated_url = "https://" + "example.com/data.csv"
urllib.request.urlopen(concatenated_url)
urllib.request.Request(concatenated_url)
nested_concatenated = "http://" + "example.com" + "/data.csv"
urllib.request.urlopen(nested_concatenated)
urllib.request.Request(nested_concatenated)

View File

@ -28,11 +28,9 @@ yaml.load("{}", SafeLoader)
yaml.load("{}", yaml.SafeLoader) yaml.load("{}", yaml.SafeLoader)
yaml.load("{}", CSafeLoader) yaml.load("{}", CSafeLoader)
yaml.load("{}", yaml.CSafeLoader) yaml.load("{}", yaml.CSafeLoader)
yaml.load("{}", yaml.cyaml.CSafeLoader)
yaml.load("{}", NewSafeLoader) yaml.load("{}", NewSafeLoader)
yaml.load("{}", Loader=SafeLoader) yaml.load("{}", Loader=SafeLoader)
yaml.load("{}", Loader=yaml.SafeLoader) yaml.load("{}", Loader=yaml.SafeLoader)
yaml.load("{}", Loader=CSafeLoader) yaml.load("{}", Loader=CSafeLoader)
yaml.load("{}", Loader=yaml.CSafeLoader) yaml.load("{}", Loader=yaml.CSafeLoader)
yaml.load("{}", Loader=yaml.cyaml.CSafeLoader)
yaml.load("{}", Loader=NewSafeLoader) yaml.load("{}", Loader=NewSafeLoader)

View File

@ -199,9 +199,6 @@ def bytes_okay(value=bytes(1)):
def int_okay(value=int("12")): def int_okay(value=int("12")):
pass pass
# Allow immutable slice()
def slice_okay(value=slice(1,2)):
pass
# Allow immutable complex() value # Allow immutable complex() value
def complex_okay(value=complex(1,2)): def complex_okay(value=complex(1,2)):

View File

@ -52,16 +52,16 @@ def not_broken5():
yield inner() yield inner()
def broken3(): def not_broken6():
return (yield from []) return (yield from [])
def broken4(): def not_broken7():
x = yield from [] x = yield from []
return x return x
def broken5(): def not_broken8():
x = None x = None
def inner(ex): def inner(ex):
@ -76,13 +76,3 @@ class NotBroken9(object):
def __await__(self): def __await__(self):
yield from function() yield from function()
return 42 return 42
async def broken6():
yield 1
return foo()
async def broken7():
yield 1
return [1, 2, 3]

View File

@ -208,17 +208,3 @@ _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
_ = f"b {t"abc" \ _ = f"b {t"abc" \
t"def"} g" t"def"} g"
# Explicit concatenation with either operand being
# a string literal that wraps across multiple lines (in parentheses)
# reports diagnostic - no autofix.
# See https://github.com/astral-sh/ruff/issues/19757
_ = "abc" + (
"def"
"ghi"
)
_ = (
"abc"
"def"
) + "ghi"

View File

@ -1,66 +0,0 @@
facts = (
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
)
facts = [
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
]
facts = {
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
}
facts = {
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
}
facts = (
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
)
facts = [
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
]
facts = {
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
}
facts = (
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
)
facts = [
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
]
facts = (
"Lobsters have blue blood.\n"
"The liver is the only human organ that can fully regenerate itself.\n"
"Clarinets are made almost entirely out of wood from the mpingo tree.\n"
"In 1971, astronaut Alan Shepard played golf on the moon.\n"
)

View File

@ -216,15 +216,3 @@ def get_items_list():
def get_items_set(): def get_items_set():
return tuple({item for item in items}) or None # OK return tuple({item for item in items}) or None # OK
# https://github.com/astral-sh/ruff/issues/21473
tuple("") or True # SIM222
tuple(t"") or True # OK
tuple(0) or True # OK
tuple(1) or True # OK
tuple(False) or True # OK
tuple(None) or True # OK
tuple(...) or True # OK
tuple(lambda x: x) or True # OK
tuple(x for x in range(0)) or True # OK

View File

@ -157,15 +157,3 @@ print(f"{1}{''}" and "bar")
# https://github.com/astral-sh/ruff/issues/7127 # https://github.com/astral-sh/ruff/issues/7127
def f(a: "'' and 'b'"): ... def f(a: "'' and 'b'"): ...
# https://github.com/astral-sh/ruff/issues/21473
tuple("") and False # SIM223
tuple(t"") and False # OK
tuple(0) and False # OK
tuple(1) and False # OK
tuple(False) and False # OK
tuple(None) and False # OK
tuple(...) and False # OK
tuple(lambda x: x) and False # OK
tuple(x for x in range(0)) and False # OK

View File

@ -137,37 +137,3 @@ os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1) os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1) os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
# See: https://github.com/astral-sh/ruff/issues/21794
import sys
if os.rename("pth1.py", "pth1.py.bak"):
print("rename: truthy")
else:
print("rename: falsey")
if os.replace("pth1.py.bak", "pth1.py"):
print("replace: truthy")
else:
print("replace: falsey")
try:
for _ in os.getcwd():
print("getcwd: iterable")
break
except TypeError as e:
print("getcwd: not iterable")
try:
for _ in os.getcwdb():
print("getcwdb: iterable")
break
except TypeError as e:
print("getcwdb: not iterable")
try:
for _ in os.readlink(sys.executable):
print("readlink: iterable")
break
except TypeError as e:
print("readlink: not iterable")

View File

@ -218,26 +218,3 @@ def should_not_fail(payload, Args):
Args: Args:
The other arguments. The other arguments.
""" """
# Test cases for Unpack[TypedDict] kwargs
from typing import TypedDict
from typing_extensions import Unpack
class User(TypedDict):
id: int
name: str
def function_with_unpack_args_should_not_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs.
Args:
query: some arg
"""
def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs but missing query arg documentation.
Args:
**kwargs: keyword arguments
"""

View File

@ -17,24 +17,3 @@ def _():
# Valid yield scope # Valid yield scope
yield 3 yield 3
# await is valid in any generator, sync or async
(await cor async for cor in f()) # ok
(await cor for cor in f()) # ok
# but not in comprehensions
[await cor async for cor in f()] # F704
{await cor async for cor in f()} # F704
{await cor: 1 async for cor in f()} # F704
[await cor for cor in f()] # F704
{await cor for cor in f()} # F704
{await cor: 1 for cor in f()} # F704
# or in the iterator of an async generator, which is evaluated in the parent
# scope
(cor async for cor in await f()) # F704
(await cor async for cor in [await c for c in f()]) # F704
# this is also okay because the comprehension is within the generator scope
([await c for c in cor] async for cor in f()) # ok

View File

@ -30,23 +30,3 @@ for a, b in d_tuple:
pass pass
for a, b in d_tuple_annotated: for a, b in d_tuple_annotated:
pass pass
# Empty dict cases
empty_dict = {}
empty_dict["x"] = 1
for k, v in empty_dict:
pass
empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
for k, v in empty_dict_annotated_tuple_keys:
pass
empty_dict_unannotated = {}
empty_dict_unannotated[("x", "y")] = True
for k, v in empty_dict_unannotated:
pass
empty_dict_annotated_str_keys: dict[str, int] = {}
empty_dict_annotated_str_keys["x"] = 1
for k, v in empty_dict_annotated_str_keys:
pass

View File

@ -129,26 +129,3 @@ def generator_with_lambda():
yield 1 yield 1
func = lambda x: x # Just a regular lambda func = lambda x: x # Just a regular lambda
yield 2 yield 2
# See: https://github.com/astral-sh/ruff/issues/21162
def foo():
def g():
yield 1
raise StopIteration # Should not trigger
def foo():
def g():
raise StopIteration # Should not trigger
yield 1
# https://github.com/astral-sh/ruff/pull/21177#pullrequestreview-3430209718
def foo():
yield 1
class C:
raise StopIteration # Should trigger
yield C
# https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
def foo():
raise StopIteration((yield 1)) # Should trigger

View File

@ -2,40 +2,15 @@ from abc import ABC, abstractmethod
from contextlib import suppress from contextlib import suppress
class MyError(Exception):
...
class MySubError(MyError):
...
class MyValueError(ValueError):
...
class MyUserWarning(UserWarning):
...
# Violation test cases with builtin errors: PLW0133
# Test case 1: Useless exception statement # Test case 1: Useless exception statement
def func(): def func():
AssertionError("This is an assertion error") # PLW0133 AssertionError("This is an assertion error") # PLW0133
MyError("This is a custom error") # PLW0133
MySubError("This is a custom error") # PLW0133
MyValueError("This is a custom value error") # PLW0133
# Test case 2: Useless exception statement in try-except block # Test case 2: Useless exception statement in try-except block
def func(): def func():
try: try:
Exception("This is an exception") # PLW0133 Exception("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
except Exception as err: except Exception as err:
pass pass
@ -44,9 +19,6 @@ def func():
def func(): def func():
if True: if True:
RuntimeError("This is an exception") # PLW0133 RuntimeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 4: Useless exception statement in class # Test case 4: Useless exception statement in class
@ -54,18 +26,12 @@ def func():
class Class: class Class:
def __init__(self): def __init__(self):
TypeError("This is an exception") # PLW0133 TypeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 5: Useless exception statement in function # Test case 5: Useless exception statement in function
def func(): def func():
def inner(): def inner():
IndexError("This is an exception") # PLW0133 IndexError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
inner() inner()
@ -74,9 +40,6 @@ def func():
def func(): def func():
while True: while True:
KeyError("This is an exception") # PLW0133 KeyError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 7: Useless exception statement in abstract class # Test case 7: Useless exception statement in abstract class
@ -85,58 +48,27 @@ def func():
@abstractmethod @abstractmethod
def method(self): def method(self):
NotImplementedError("This is an exception") # PLW0133 NotImplementedError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 8: Useless exception statement inside context manager # Test case 8: Useless exception statement inside context manager
def func(): def func():
with suppress(Exception): with suppress(AttributeError):
AttributeError("This is an exception") # PLW0133 AttributeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 9: Useless exception statement in parentheses # Test case 9: Useless exception statement in parentheses
def func(): def func():
(RuntimeError("This is an exception")) # PLW0133 (RuntimeError("This is an exception")) # PLW0133
(MyError("This is an exception")) # PLW0133
(MySubError("This is an exception")) # PLW0133
(MyValueError("This is an exception")) # PLW0133
# Test case 10: Useless exception statement in continuation # Test case 10: Useless exception statement in continuation
def func(): def func():
x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133 x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
x = 1; (MyError("This is an exception")); y = 2 # PLW0133
x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
# Test case 11: Useless warning statement # Test case 11: Useless warning statement
def func(): def func():
UserWarning("This is a user warning") # PLW0133 UserWarning("This is an assertion error") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Test case 12: Useless exception statement at module level
import builtins
builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
PythonFinalizationError("Added in Python 3.13") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
UserWarning("This is a user warning") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Non-violation test cases: PLW0133 # Non-violation test cases: PLW0133
@ -187,3 +119,10 @@ def func():
def func(): def func():
with suppress(AttributeError): with suppress(AttributeError):
raise AttributeError("This is an exception") # OK raise AttributeError("This is an exception") # OK
import builtins
builtins.TypeError("still an exception even though it's an Attribute")
PythonFinalizationError("Added in Python 3.13")

View File

@ -132,6 +132,7 @@ async def c():
# Non-errors # Non-errors
### ###
# False-negative: RustPython doesn't parse the `\N{snowman}`.
"\N{snowman} {}".format(a) "\N{snowman} {}".format(a)
"{".format(a) "{".format(a)
@ -275,6 +276,3 @@ if __name__ == "__main__":
number = 0 number = 0
string = "{}".format(number := number + 1) string = "{}".format(number := number + 1)
print(string) print(string)
# Unicode escape
"\N{angle}AOB = {angle}°".format(angle=180)

View File

@ -138,6 +138,5 @@ with open("file.txt", encoding="utf-8") as f:
with open("file.txt", encoding="utf-8") as f: with open("file.txt", encoding="utf-8") as f:
contents = process_contents(f.read()) contents = process_contents(f.read())
with open("file1.txt", encoding="utf-8") as f: with open("file.txt", encoding="utf-8") as f:
contents: str = process_contents(f.read()) contents: str = process_contents(f.read())

View File

@ -1,8 +0,0 @@
from pathlib import Path
with Path("file.txt").open() as f:
contents = f.read()
with Path("file.txt").open("r") as f:
contents = f.read()

View File

@ -1,26 +0,0 @@
from pathlib import Path
with Path("file.txt").open("w") as f:
f.write("test")
with Path("file.txt").open("wb") as f:
f.write(b"test")
with Path("file.txt").open(mode="w") as f:
f.write("test")
with Path("file.txt").open("w", encoding="utf8") as f:
f.write("test")
with Path("file.txt").open("w", errors="ignore") as f:
f.write("test")
with Path(foo()).open("w") as f:
f.write("test")
p = Path("file.txt")
with p.open("w") as f:
f.write("test")
with Path("foo", "bar", "baz").open("w") as f:
f.write("test")

View File

@ -1,109 +0,0 @@
# Correct usage in loop and comprehension
def process_data():
return 42
def test_correct_dummy_usage():
my_list = [{"foo": 1}, {"foo": 2}]
# Should NOT detect - dummy variable is not used
[process_data() for _ in my_list] # OK: `_` is ignored by rule
# Should NOT detect - dummy variable is not used
[item["foo"] for item in my_list] # OK: not a dummy variable name
# Should NOT detect - dummy variable is not used
[42 for _unused in my_list] # OK: `_unused` is not accessed
# Regular For Loops
def test_for_loops():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
for _item in my_list:
print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable
for _index, _value in enumerate(my_list):
result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
# List Comprehensions
def test_list_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable in nested comprehension
nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
# RUF052: Both `_item` and `_sublist` are accessed
# Should detect with conditions
filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
# RUF052: Local dummy variable `_item` is accessed
# Dict Comprehensions
def test_dict_comprehensions():
my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
# Should detect used dummy variable
result = {_item["key"]: _item["value"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with enumerate
indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
# RUF052: Both `_index` and `_item` are accessed
# Should detect in nested dict comprehension
nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
for _outer, sublist in enumerate([my_list])}
# RUF052: `_outer`, `_inner` are accessed
# Set Comprehensions
def test_set_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
# Should detect used dummy variable
unique_values = {_item["foo"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with conditions
filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with complex expression
processed = {_item["foo"] * 2 for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Generator Expressions
def test_generator_expressions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
gen = (_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect when passed to function
total = sum(_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect with multiple generators
pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
# RUF052: Both `_x` and `_y` are accessed
# Should detect in nested generator
nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
# RUF052: `_inner` and `_sublist` are accessed
# Complex Examples with Multiple Comprehension Types
def test_mixed_comprehensions():
data = [{"items": [1, 2, 3]}, {"items": [4, 5, 6]}]
# Should detect in mixed comprehensions
result = [
{_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
for _record in data
]
# RUF052: `_key`, `_val`, and `_record` are all accessed
# Should detect in generator passed to list constructor
gen_list = list(_item["items"][0] for _item in data)
# RUF052: Local dummy variable `_item` is accessed

View File

@ -1,70 +0,0 @@
import abc
import typing
class User: # Test normal class properties
@property
def name(self): # ERROR: No return
f"{self.first_name} {self.last_name}"
@property
def age(self): # OK: Returning something
return 100
def method(self): # OK: Not a property
x = 1
@property
def nested(self): # ERROR: Property itself doesn't return
def inner():
return 0
@property
def stub(self): ... # OK: A stub; doesn't return anything
class UserMeta(metaclass=abc.ABCMeta): # Test properies inside of an ABC class
@property
@abc.abstractmethod
def abstr_prop1(self): ... # OK: Abstract methods doesn't need to return anything
@property
@abc.abstractmethod
def abstr_prop2(self): # OK: Abstract methods doesn't need to return anything
"""
A cool docstring
"""
@property
def prop1(self): # OK: Returning a value
return 1
@property
def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
50
def method(self): # OK: Not a property
x = 1
def func(): # OK: Not a property
x = 1
class Proto(typing.Protocol): # Tests for a Protocol class
@property
def prop1(self) -> int: ... # OK: A stub property
class File: # Extra tests for things like yield/yield from/raise
@property
def stream1(self): # OK: Yields something
yield
@property
def stream2(self): # OK: Yields from something
yield from self.stream1
@property
def children(self): # OK: Raises
raise ValueError("File does not have children")

View File

@ -1,88 +0,0 @@
def f():
# These should both be ignored by the range suppression.
# ruff: disable[E741, F841]
I = 1
# ruff: enable[E741, F841]
def f():
# These should both be ignored by the implicit range suppression.
# Should also generate an "unmatched suppression" warning.
# ruff:disable[E741,F841]
I = 1
def f():
# Neither warning is ignored, and an "unmatched suppression"
# should be generated.
I = 1
# ruff: enable[E741, F841]
def f():
# One should be ignored by the range suppression, and
# the other logged to the user.
# ruff: disable[E741]
I = 1
# ruff: enable[E741]
def f():
# Test interleaved range suppressions. The first and last
# lines should each log a different warning, while the
# middle line should be completely silenced.
# ruff: disable[E741]
l = 0
# ruff: disable[F841]
O = 1
# ruff: enable[E741]
I = 2
# ruff: enable[F841]
def f():
# Neither of these are ignored and warnings are
# logged to user
# ruff: disable[E501]
I = 1
# ruff: enable[E501]
def f():
# These should both be ignored by the range suppression,
# and an unusued noqa diagnostic should be logged.
# ruff:disable[E741,F841]
I = 1 # noqa: E741,F841
# ruff:enable[E741,F841]
def f():
# TODO: Duplicate codes should be counted as duplicate, not unused
# ruff: disable[F841, F841]
foo = 0
def f():
# Overlapping range suppressions, one should be marked as used,
# and the other should trigger an unused suppression diagnostic
# ruff: disable[F841]
# ruff: disable[F841]
foo = 0
def f():
# Multiple codes but only one is used
# ruff: disable[E741, F401, F841]
foo = 0
def f():
# Multiple codes but only two are used
# ruff: disable[E741, F401, F841]
I = 0
def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")

View File

@ -1,38 +0,0 @@
a: int = 1
def f1():
global a
a: str = "foo" # error
b: int = 1
def outer():
def inner():
global b
b: str = "nested" # error
c: int = 1
def f2():
global c
c: list[str] = [] # error
d: int = 1
def f3():
global d
d: str # error
e: int = 1
def f4():
e: str = "happy" # okay
global f
f: int = 1 # okay
g: int = 1
global g # error
class C:
x: str
global x # error
class D:
global x # error
x: str

View File

@ -3,5 +3,3 @@ def func():
# Top-level await # Top-level await
await 1 await 1
([await c for c in cor] async for cor in func()) # ok

View File

@ -1,24 +0,0 @@
async def gen():
yield 1
return 42
def gen(): # B901 but not a syntax error - not an async generator
yield 1
return 42
async def gen(): # ok - no value in return
yield 1
return
async def gen():
yield 1
return foo()
async def gen():
yield 1
return [1, 2, 3]
async def gen():
if True:
yield 1
return 10

View File

@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors. Found 7 errors.
[*] 7 potentially fixable with the `--fix` option. [*] 7 potentially fixable with the --fix option.
``` ```
Running from the project directory itself should exhibit the same behavior: Running from the project directory itself should exhibit the same behavior:
@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but n
project/file.py:1:8: F401 [*] `os` imported but unused project/file.py:1:8: F401 [*] `os` imported but unused
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors. Found 7 errors.
[*] 7 potentially fixable with the `--fix` option. [*] 7 potentially fixable with the --fix option.
``` ```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level Running from the sub-package directory should exhibit the same behavior, but omit the top-level
@ -43,7 +43,7 @@ files:
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
Found 2 errors. Found 2 errors.
[*] 2 potentially fixable with the `--fix` option. [*] 2 potentially fixable with the --fix option.
``` ```
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve `--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
@ -61,7 +61,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:4:27: F401
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
Found 9 errors. Found 9 errors.
[*] 9 potentially fixable with the `--fix` option. [*] 9 potentially fixable with the --fix option.
``` ```
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
Found 4 errors. Found 4 errors.
[*] 4 potentially fixable with the `--fix` option. [*] 4 potentially fixable with the --fix option.
``` ```
Passing an excluded directory directly should report errors in the contained files: Passing an excluded directory directly should report errors in the contained files:
@ -83,7 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/ ∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
Found 1 error. Found 1 error.
[*] 1 potentially fixable with the `--fix` option. [*] 1 potentially fixable with the --fix option.
``` ```
Unless we `--force-exclude`: Unless we `--force-exclude`:

View File

@ -214,13 +214,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
range: _, range: _,
node_index: _, node_index: _,
}) => { }) => {
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
checker,
expr,
elts,
);
}
if ctx.is_store() { if ctx.is_store() {
let check_too_many_expressions = let check_too_many_expressions =
checker.is_rule_enabled(Rule::ExpressionsInStarAssignment); checker.is_rule_enabled(Rule::ExpressionsInStarAssignment);
@ -1336,13 +1329,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
} }
} }
Expr::Set(set) => { Expr::Set(set) => {
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
checker,
expr,
&set.elts,
);
}
if checker.is_rule_enabled(Rule::DuplicateValue) { if checker.is_rule_enabled(Rule::DuplicateValue) {
flake8_bugbear::rules::duplicate_value(checker, set); flake8_bugbear::rules::duplicate_value(checker, set);
} }

View File

@ -131,9 +131,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) { if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) {
flake8_pyi::rules::bad_generator_return_type(function_def, checker); flake8_pyi::rules::bad_generator_return_type(function_def, checker);
} }
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, function_def);
}
if checker.source_type.is_stub() { if checker.source_type.is_stub() {
if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) { if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt); flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
@ -347,9 +344,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::InvalidArgumentName) { if checker.is_rule_enabled(Rule::InvalidArgumentName) {
pep8_naming::rules::invalid_argument_name_function(checker, function_def); pep8_naming::rules::invalid_argument_name_function(checker, function_def);
} }
if checker.is_rule_enabled(Rule::PropertyWithoutReturn) {
ruff::rules::property_without_return(checker, function_def);
}
} }
Stmt::Return(_) => { Stmt::Return(_) => {
if checker.is_rule_enabled(Rule::ReturnInInit) { if checker.is_rule_enabled(Rule::ReturnInInit) {
@ -956,6 +950,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::MisplacedBareRaise) { if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise); pylint::rules::misplaced_bare_raise(checker, raise);
} }
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, raise);
}
} }
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => { Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
if checker.is_rule_enabled(Rule::GlobalStatement) { if checker.is_rule_enabled(Rule::GlobalStatement) {

View File

@ -35,7 +35,6 @@ use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::str::Quote; use ruff_python_ast::str::Quote;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern}; use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern};
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr, self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
@ -49,7 +48,7 @@ use ruff_python_parser::semantic_errors::{
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind,
}; };
use ruff_python_parser::typing::{AnnotationKind, ParsedAnnotation, parse_type_annotation}; use ruff_python_parser::typing::{AnnotationKind, ParsedAnnotation, parse_type_annotation};
use ruff_python_parser::{ParseError, Parsed}; use ruff_python_parser::{ParseError, Parsed, Tokens};
use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags}; use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
use ruff_python_semantic::analyze::{imports, typing}; use ruff_python_semantic::analyze::{imports, typing};
use ruff_python_semantic::{ use ruff_python_semantic::{
@ -69,7 +68,6 @@ use crate::noqa::NoqaMapping;
use crate::package::PackageRoot; use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled; use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::flake8_bugbear::rules::ReturnInGenerator;
use crate::rules::pyflakes::rules::{ use crate::rules::pyflakes::rules::{
LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction, LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction,
UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction, UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
@ -437,15 +435,6 @@ impl<'a> Checker<'a> {
} }
} }
/// Returns the [`Tokens`] for the parsed source file.
///
///
/// Unlike [`Self::tokens`], this method always returns
/// the tokens for the current file, even when within a parsed type annotation.
pub(crate) fn source_tokens(&self) -> &'a Tokens {
self.parsed.tokens()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte /// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets. /// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> { pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@ -739,12 +728,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(NonlocalWithoutBinding { name }, error.range); self.report_diagnostic(NonlocalWithoutBinding { name }, error.range);
} }
} }
SemanticSyntaxErrorKind::ReturnInGenerator => {
// B901
if self.is_rule_enabled(Rule::ReturnInGenerator) {
self.report_diagnostic(ReturnInGenerator, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_) | SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@ -763,7 +746,6 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. } | SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_) | SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_) | SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::TypeParameterDefaultOrder(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => { | SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error); self.semantic_errors.borrow_mut().push(error);
} }
@ -797,10 +779,6 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind { match scope.kind {
ScopeKind::Class(_) => return false, ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true, ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
} => return true,
ScopeKind::Generator { .. } ScopeKind::Generator { .. }
| ScopeKind::Module | ScopeKind::Module
| ScopeKind::Type | ScopeKind::Type
@ -850,19 +828,14 @@ impl SemanticSyntaxContext for Checker<'_> {
self.source_type.is_ipynb() self.source_type.is_ipynb()
} }
fn in_generator_context(&self) -> bool { fn in_generator_scope(&self) -> bool {
for scope in self.semantic.current_scopes() { matches!(
if matches!( &self.semantic.current_scope().kind,
scope.kind, ScopeKind::Generator {
ScopeKind::Generator { kind: GeneratorKind::Generator,
kind: GeneratorKind::Generator, ..
..
}
) {
return true;
} }
} )
false
} }
fn in_loop_context(&self) -> bool { fn in_loop_context(&self) -> bool {

View File

@ -1,6 +1,6 @@
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};

View File

@ -12,20 +12,17 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{ use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping, Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
}; };
use crate::preview::is_range_suppressions_enabled;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target; use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks; use crate::rules::pygrep_hooks;
use crate::rules::ruff; use crate::rules::ruff;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::suppression::Suppressions;
use crate::{Edit, Fix, Locator}; use crate::{Edit, Fix, Locator};
use super::ast::LintContext; use super::ast::LintContext;
/// RUF100 /// RUF100
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_noqa( pub(crate) fn check_noqa(
context: &mut LintContext, context: &mut LintContext,
path: &Path, path: &Path,
@ -34,7 +31,6 @@ pub(crate) fn check_noqa(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
analyze_directives: bool, analyze_directives: bool,
settings: &LinterSettings, settings: &LinterSettings,
suppressions: &Suppressions,
) -> Vec<usize> { ) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file). // Identify any codes that are globally exempted (within the current file).
let file_noqa_directives = let file_noqa_directives =
@ -44,7 +40,7 @@ pub(crate) fn check_noqa(
let mut noqa_directives = let mut noqa_directives =
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator); NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
if file_noqa_directives.is_empty() && noqa_directives.is_empty() && suppressions.is_empty() { if file_noqa_directives.is_empty() && noqa_directives.is_empty() {
return Vec::new(); return Vec::new();
} }
@ -64,19 +60,11 @@ pub(crate) fn check_noqa(
continue; continue;
} }
// Apply file-level suppressions first
if exemption.contains_secondary_code(code) { if exemption.contains_secondary_code(code) {
ignored_diagnostics.push(index); ignored_diagnostics.push(index);
continue; continue;
} }
// Apply ranged suppressions next
if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) {
ignored_diagnostics.push(index);
continue;
}
// Apply end-of-line noqa suppressions last
let noqa_offsets = diagnostic let noqa_offsets = diagnostic
.parent() .parent()
.into_iter() .into_iter()
@ -119,9 +107,6 @@ 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 // Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed. // suppressed.
if context.is_rule_enabled(Rule::UnusedNOQA) if context.is_rule_enabled(Rule::UnusedNOQA)
@ -143,13 +128,8 @@ pub(crate) fn check_noqa(
Directive::All(directive) => { Directive::All(directive) => {
if matches.is_empty() { if matches.is_empty() {
let edit = delete_comment(directive.range(), locator); let edit = delete_comment(directive.range(), locator);
let mut diagnostic = context.report_diagnostic( let mut diagnostic = context
UnusedNOQA { .report_diagnostic(UnusedNOQA { codes: None }, directive.range());
codes: None,
kind: ruff::rules::UnusedNOQAKind::Noqa,
},
directive.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.set_fix(Fix::safe_edit(edit)); diagnostic.set_fix(Fix::safe_edit(edit));
} }
@ -244,7 +224,6 @@ pub(crate) fn check_noqa(
.map(|code| (*code).to_string()) .map(|code| (*code).to_string())
.collect(), .collect(),
}), }),
kind: ruff::rules::UnusedNOQAKind::Noqa,
}, },
directive.range(), directive.range(),
); );

View File

@ -4,9 +4,9 @@ use std::path::Path;
use ruff_notebook::CellOffsets; use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType; use ruff_python_ast::PySourceType;
use ruff_python_ast::token::Tokens;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::Tokens;
use crate::Locator; use crate::Locator;
use crate::directives::TodoComment; use crate::directives::TodoComment;

View File

@ -454,7 +454,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation, (Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation, (Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation, (Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation,
(Flake8ImplicitStrConcat, "004") => rules::flake8_implicit_str_concat::rules::ImplicitStringConcatenationInCollectionLiteral,
// flake8-print // flake8-print
(Flake8Print, "1") => rules::flake8_print::rules::Print, (Flake8Print, "1") => rules::flake8_print::rules::Print,
@ -1059,7 +1058,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict, (Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict,
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions, (Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion, (Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
(Ruff, "100") => rules::ruff::rules::UnusedNOQA, (Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA, (Ruff, "101") => rules::ruff::rules::RedirectedNOQA,

View File

@ -5,8 +5,8 @@ use std::str::FromStr;
use bitflags::bitflags; use bitflags::bitflags;
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};

View File

@ -5,8 +5,8 @@ use std::iter::FusedIterator;
use std::slice::Iter; use std::slice::Iter;
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
use ruff_python_ast::token::{Token, TokenKind, Tokens};
use ruff_python_ast::{self as ast, Stmt, Suite}; use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_python_parser::{Token, TokenKind, Tokens};
use ruff_source_file::UniversalNewlineIterator; use ruff_source_file::UniversalNewlineIterator;
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};

View File

@ -3,13 +3,14 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::token::{self, Tokens, parenthesized_range}; use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::textwrap::dedent_to;
use ruff_python_trivia::{ use ruff_python_trivia::{
PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content,
is_python_whitespace,
}; };
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -208,7 +209,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments, arguments: &Arguments,
parentheses: Parentheses, parentheses: Parentheses,
source: &str, source: &str,
tokens: &Tokens, comment_ranges: &CommentRanges,
) -> Result<Edit> { ) -> Result<Edit> {
// Partition into arguments before and after the argument to remove. // Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments let (before, after): (Vec<_>, Vec<_>) = arguments
@ -223,7 +224,7 @@ pub(crate) fn remove_argument<T: Ranged>(
.context("Unable to find argument")?; .context("Unable to find argument")?;
let parenthesized_range = let parenthesized_range =
token::parenthesized_range(arg.value().into(), arguments.into(), tokens) parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source)
.unwrap_or(arg.range()); .unwrap_or(arg.range());
if !after.is_empty() { if !after.is_empty() {
@ -269,14 +270,25 @@ pub(crate) fn remove_argument<T: Ranged>(
/// ///
/// The new argument will be inserted before the first existing keyword argument in `arguments`, if /// The new argument will be inserted before the first existing keyword argument in `arguments`, if
/// there are any present. Otherwise, the new argument is added to the end of the argument list. /// there are any present. Otherwise, the new argument is added to the end of the argument list.
pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit { pub(crate) fn add_argument(
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() {
let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range); let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source)
.unwrap_or(*range);
Edit::insertion(format!("{argument}, "), keyword.start()) Edit::insertion(format!("{argument}, "), keyword.start())
} else if let Some(last) = arguments.arguments_source_order().last() { } else if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument. // Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range(last.value().into(), arguments.into(), tokens) let last = parenthesized_range(
.unwrap_or(last.range()); last.value().into(),
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end()) Edit::insertion(format!(", {argument}"), last.end())
} else { } else {
// Case 2: no arguments. Add argument, without any trailing comma. // Case 2: no arguments. Add argument, without any trailing comma.
@ -286,7 +298,12 @@ pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Token
/// Generic function to add a (regular) parameter to a function definition. /// Generic function to add a (regular) parameter to a function definition.
pub(crate) fn add_parameter(parameter: &str, parameters: &Parameters, source: &str) -> Edit { pub(crate) fn add_parameter(parameter: &str, parameters: &Parameters, source: &str) -> Edit {
if let Some(last) = parameters.args.iter().rfind(|arg| arg.default.is_none()) { if let Some(last) = parameters
.args
.iter()
.filter(|arg| arg.default.is_none())
.next_back()
{
// Case 1: at least one regular parameter, so append after the last one. // Case 1: at least one regular parameter, so append after the last one.
Edit::insertion(format!(", {parameter}"), last.end()) Edit::insertion(format!(", {parameter}"), last.end())
} else if !parameters.args.is_empty() { } else if !parameters.args.is_empty() {

View File

@ -9,11 +9,10 @@ use anyhow::Result;
use libcst_native as cst; use libcst_native as cst;
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt}; use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_importer::Insertion; use ruff_python_importer::Insertion;
use ruff_python_parser::Parsed; use ruff_python_parser::{Parsed, Tokens};
use ruff_python_semantic::{ use ruff_python_semantic::{
ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel, ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel,
}; };

View File

@ -46,7 +46,6 @@ pub mod rule_selector;
pub mod rules; pub mod rules;
pub mod settings; pub mod settings;
pub mod source_kind; pub mod source_kind;
pub mod suppression;
mod text_helpers; mod text_helpers;
pub mod upstream_categories; pub mod upstream_categories;
mod violation; mod violation;

View File

@ -32,7 +32,6 @@ use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, TargetVersion, flags}; use crate::settings::{LinterSettings, TargetVersion, flags};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Locator, directives, fs}; use crate::{Locator, directives, fs};
pub(crate) mod float; pub(crate) mod float;
@ -129,7 +128,6 @@ pub fn check_path(
source_type: PySourceType, source_type: PySourceType,
parsed: &Parsed<ModModule>, parsed: &Parsed<ModModule>,
target_version: TargetVersion, target_version: TargetVersion,
suppressions: &Suppressions,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
// Aggregate all diagnostics. // Aggregate all diagnostics.
let mut context = LintContext::new(path, locator.contents(), settings); let mut context = LintContext::new(path, locator.contents(), settings);
@ -341,7 +339,6 @@ pub fn check_path(
&directives.noqa_line_for, &directives.noqa_line_for,
parsed.has_valid_syntax(), parsed.has_valid_syntax(),
settings, settings,
suppressions,
); );
if noqa.is_enabled() { if noqa.is_enabled() {
for index in ignored.iter().rev() { for index in ignored.iter().rev() {
@ -403,9 +400,6 @@ pub fn add_noqa_to_path(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics, ignoring any existing `noqa` directives. // Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -420,7 +414,6 @@ pub fn add_noqa_to_path(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
// Add any missing `# noqa` pragmas. // Add any missing `# noqa` pragmas.
@ -434,7 +427,6 @@ pub fn add_noqa_to_path(
&directives.noqa_line_for, &directives.noqa_line_for,
stylist.line_ending(), stylist.line_ending(),
reason, reason,
&suppressions,
) )
} }
@ -469,9 +461,6 @@ pub fn lint_only(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -486,7 +475,6 @@ pub fn lint_only(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
LinterResult { LinterResult {
@ -578,9 +566,6 @@ pub fn lint_fix<'a>(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -595,7 +580,6 @@ pub fn lint_fix<'a>(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
if iterations == 0 { if iterations == 0 {
@ -785,7 +769,6 @@ mod tests {
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet}; use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet};
use crate::{Locator, assert_diagnostics, directives, settings}; use crate::{Locator, assert_diagnostics, directives, settings};
@ -961,7 +944,6 @@ mod tests {
&locator, &locator,
&indexer, &indexer,
); );
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let mut diagnostics = check_path( let mut diagnostics = check_path(
path, path,
None, None,
@ -975,7 +957,6 @@ mod tests {
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
diagnostics.sort_by(Diagnostic::ruff_start_ordering); diagnostics.sort_by(Diagnostic::ruff_start_ordering);
diagnostics diagnostics
@ -1001,7 +982,6 @@ mod tests {
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)] #[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)] #[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)] #[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
#[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)]
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> { fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
let snapshot = format!( let snapshot = format!(
"semantic_syntax_error_{}_{}", "semantic_syntax_error_{}_{}",
@ -1063,7 +1043,6 @@ mod tests {
Rule::YieldFromInAsyncFunction, Rule::YieldFromInAsyncFunction,
Path::new("yield_from_in_async_function.py") Path::new("yield_from_in_async_function.py")
)] )]
#[test_case(Rule::ReturnInGenerator, Path::new("return_in_generator.py"))]
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> { fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string(); let snapshot = path.to_string_lossy().to_string();
let path = Path::new("resources/test/fixtures/syntax_errors").join(path); let path = Path::new("resources/test/fixtures/syntax_errors").join(path);

View File

@ -20,14 +20,12 @@ use crate::Locator;
use crate::fs::relativize_path; use crate::fs::relativize_path;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target; use crate::rule_redirects::get_redirect_target;
use crate::suppression::Suppressions;
/// Generates an array of edits that matches the length of `messages`. /// Generates an array of edits that matches the length of `messages`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic. /// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide /// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied /// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously. /// simultaneously.
#[expect(clippy::too_many_arguments)]
pub fn generate_noqa_edits( pub fn generate_noqa_edits(
path: &Path, path: &Path,
diagnostics: &[Diagnostic], diagnostics: &[Diagnostic],
@ -36,19 +34,11 @@ pub fn generate_noqa_edits(
external: &[String], external: &[String],
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
suppressions: &Suppressions,
) -> Vec<Option<Edit>> { ) -> Vec<Option<Edit>> {
let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path);
let exemption = FileExemption::from(&file_directives); let exemption = FileExemption::from(&file_directives);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments( let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None) build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
} }
@ -735,7 +725,6 @@ pub(crate) fn add_noqa(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
reason: Option<&str>, reason: Option<&str>,
suppressions: &Suppressions,
) -> Result<usize> { ) -> Result<usize> {
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
@ -746,7 +735,6 @@ pub(crate) fn add_noqa(
noqa_line_for, noqa_line_for,
line_ending, line_ending,
reason, reason,
suppressions,
); );
fs::write(path, output)?; fs::write(path, output)?;
@ -763,7 +751,6 @@ fn add_noqa_inner(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
reason: Option<&str>, reason: Option<&str>,
suppressions: &Suppressions,
) -> (usize, String) { ) -> (usize, String) {
let mut count = 0; let mut count = 0;
@ -773,14 +760,7 @@ fn add_noqa_inner(
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments( let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason); let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
@ -879,7 +859,6 @@ fn find_noqa_comments<'a>(
exemption: &'a FileExemption, exemption: &'a FileExemption,
directives: &'a NoqaDirectives, directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
suppressions: &'a Suppressions,
) -> Vec<Option<NoqaComment<'a>>> { ) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `messages` // List of noqa comments, ordered to match up with `messages`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![]; let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
@ -896,12 +875,6 @@ fn find_noqa_comments<'a>(
continue; continue;
} }
// Apply ranged suppressions next
if suppressions.check_diagnostic(message) {
comments_by_line.push(None);
continue;
}
// Is the violation ignored by a `noqa` directive on the parent line? // Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent) = message.parent() { if let Some(parent) = message.parent() {
if let Some(directive_line) = if let Some(directive_line) =
@ -1280,7 +1253,6 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting; use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::suppression::Suppressions;
use crate::{Edit, Violation}; use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits}; use crate::{Locator, generate_noqa_edits};
@ -2876,7 +2848,6 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 0); assert_eq!(count, 0);
assert_eq!(output, format!("{contents}")); assert_eq!(output, format!("{contents}"));
@ -2901,7 +2872,6 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n"); assert_eq!(output, "x = 1 # noqa: F841\n");
@ -2933,7 +2903,6 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n"); assert_eq!(output, "x = 1 # noqa: E741, F841\n");
@ -2965,7 +2934,6 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 0); assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa"); assert_eq!(output, "x = 1 # noqa");
@ -2988,7 +2956,6 @@ print(
let messages = [PrintfStringFormatting let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)]; .into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&messages, &messages,
@ -2997,7 +2964,6 @@ print(
&[], &[],
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
&suppressions,
); );
assert_eq!( assert_eq!(
edits, edits,
@ -3021,7 +2987,6 @@ bar =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)]; [UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&messages, &messages,
@ -3030,7 +2995,6 @@ bar =
&[], &[],
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
&suppressions,
); );
assert_eq!( assert_eq!(
edits, edits,

View File

@ -9,11 +9,6 @@ use crate::settings::LinterSettings;
// Rule-specific behavior // Rule-specific behavior
// https://github.com/astral-sh/ruff/pull/21382
pub(crate) const fn is_custom_exception_checking_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/15541 // https://github.com/astral-sh/ruff/pull/15541
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
@ -284,15 +279,3 @@ pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &Linte
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
} }
// https://github.com/astral-sh/ruff/pull/21469
pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21623
pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -22,7 +22,6 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
# Case-sensitive # Case-sensitive
pyright pyright
| pyrefly | pyrefly
| ruff\s*:\s*(disable|enable)
| mypy: | mypy:
| type:\s*ignore | type:\s*ignore
| SPDX-License-Identifier: | SPDX-License-Identifier:
@ -149,8 +148,6 @@ mod tests {
assert!(!comment_contains_code("# 123", &[])); assert!(!comment_contains_code("# 123", &[]));
assert!(!comment_contains_code("# 123.1", &[])); assert!(!comment_contains_code("# 123.1", &[]));
assert!(!comment_contains_code("# 1, 2, 3", &[])); assert!(!comment_contains_code("# 1, 2, 3", &[]));
assert!(!comment_contains_code("# ruff: disable[E501]", &[]));
assert!(!comment_contains_code("#ruff:enable[E501, F84]", &[]));
assert!(!comment_contains_code( assert!(!comment_contains_code(
"# pylint: disable=redefined-outer-name", "# pylint: disable=redefined-outer-name",
&[] &[]

View File

@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def:
response_model_arg, response_model_arg,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.source(), checker.locator().contents(),
checker.tokens(), checker.comment_ranges(),
) )
.map(Fix::unsafe_edit) .map(Fix::unsafe_edit)
}); });

View File

@ -70,7 +70,7 @@ fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
} }
/// Returns `true` if an expression resolves to a call to `pathlib.Path.open`. /// Returns `true` if an expression resolves to a call to `pathlib.Path.open`.
pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool { fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else { let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
return false; return false;
}; };

View File

@ -18,7 +18,7 @@ mod async_zero_sleep;
mod blocking_http_call; mod blocking_http_call;
mod blocking_http_call_httpx; mod blocking_http_call_httpx;
mod blocking_input; mod blocking_input;
pub(crate) mod blocking_open_call; mod blocking_open_call;
mod blocking_path_methods; mod blocking_path_methods;
mod blocking_process_invocation; mod blocking_process_invocation;
mod blocking_sleep; mod blocking_sleep;

View File

@ -10,11 +10,11 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use test_case::test_case; use test_case::test_case;
use crate::assert_diagnostics;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode; use crate::settings::types::PreviewMode;
use crate::test::test_path; use crate::test::test_path;
use crate::{assert_diagnostics, assert_diagnostics_diff};
#[test_case(Rule::Assert, Path::new("S101.py"))] #[test_case(Rule::Assert, Path::new("S101.py"))]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"))] #[test_case(Rule::BadFilePermissions, Path::new("S103.py"))]
@ -112,19 +112,14 @@ mod tests {
rule_code.noqa_code(), rule_code.noqa_code(),
path.to_string_lossy() path.to_string_lossy()
); );
let diagnostics = test_path(
assert_diagnostics_diff!(
snapshot,
Path::new("flake8_bandit").join(path).as_path(), Path::new("flake8_bandit").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Disabled,
..LinterSettings::for_rule(rule_code)
},
&LinterSettings { &LinterSettings {
preview: PreviewMode::Enabled, preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code) ..LinterSettings::for_rule(rule_code)
} },
); )?;
assert_diagnostics!(snapshot, diagnostics);
Ok(()) Ok(())
} }

View File

@ -4,16 +4,11 @@
use itertools::Either; use itertools::Either;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator}; use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::find_binding_value;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::Violation; use crate::Violation;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::preview::{ use crate::preview::is_suspicious_function_reference_enabled;
is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled,
};
use crate::settings::LinterSettings;
/// ## What it does /// ## What it does
/// Checks for calls to `pickle` functions or modules that wrap them. /// Checks for calls to `pickle` functions or modules that wrap them.
@ -1021,25 +1016,6 @@ fn suspicious_function(
|| has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://") || has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://")
} }
/// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix.
fn expression_starts_with_http_prefix(
expr: &Expr,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> bool {
let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings)
&& let Some(name_expr) = expr.as_name_expr()
&& let Some(binding_id) = semantic.only_binding(name_expr)
&& let Some(value) = find_binding_value(semantic.binding(binding_id), semantic)
{
value
} else {
expr
};
leading_chars(resolved_expression).is_some_and(has_http_prefix)
}
/// Return the leading characters for an expression, if it's a string literal, f-string, or /// Return the leading characters for an expression, if it's a string literal, f-string, or
/// string concatenation. /// string concatenation.
fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> { fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> {
@ -1163,19 +1139,17 @@ fn suspicious_function(
// URLOpen (`Request`) // URLOpen (`Request`)
["urllib", "request", "Request"] | ["six", "moves", "urllib", "request", "Request"] => { ["urllib", "request", "Request"] | ["six", "moves", "urllib", "request", "Request"] => {
if let Some(arguments) = arguments { if let Some(arguments) = arguments {
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes. // If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
if arguments.args.iter().all(|arg| !arg.is_starred_expr()) if arguments.args.iter().all(|arg| !arg.is_starred_expr())
&& arguments && arguments
.keywords .keywords
.iter() .iter()
.all(|keyword| keyword.arg.is_some()) .all(|keyword| keyword.arg.is_some())
{ {
if let Some(url_expr) = arguments.find_argument_value("url", 0) if arguments
&& expression_starts_with_http_prefix( .find_argument_value("url", 0)
url_expr, .and_then(leading_chars)
checker.semantic(), .is_some_and(has_http_prefix)
checker.settings(),
)
{ {
return; return;
} }
@ -1212,25 +1186,19 @@ fn suspicious_function(
name.segments() == ["urllib", "request", "Request"] name.segments() == ["urllib", "request", "Request"]
}) })
{ {
if let Some(url_expr) = arguments.find_argument_value("url", 0) if arguments
&& expression_starts_with_http_prefix( .find_argument_value("url", 0)
url_expr, .and_then(leading_chars)
checker.semantic(), .is_some_and(has_http_prefix)
checker.settings(),
)
{ {
return; return;
} }
} }
} }
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes. // If the `url` argument is a string literal, allow `http` and `https` schemes.
Some(expr) => { Some(expr) => {
if expression_starts_with_http_prefix( if leading_chars(expr).is_some_and(has_http_prefix) {
expr,
checker.semantic(),
checker.settings(),
) {
return; return;
} }
} }

View File

@ -12,7 +12,7 @@ use crate::{checkers::ast::Checker, settings::LinterSettings};
/// Checks for non-literal strings being passed to [`markupsafe.Markup`][markupsafe-markup]. /// Checks for non-literal strings being passed to [`markupsafe.Markup`][markupsafe-markup].
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// [`markupsafe.Markup`][markupsafe-markup] does not perform any escaping, so passing dynamic /// [`markupsafe.Markup`] does not perform any escaping, so passing dynamic
/// content, like f-strings, variables or interpolated strings will potentially /// content, like f-strings, variables or interpolated strings will potentially
/// lead to XSS vulnerabilities. /// lead to XSS vulnerabilities.
/// ///

View File

@ -75,7 +75,6 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) {
qualified_name.segments(), qualified_name.segments(),
["yaml", "SafeLoader" | "CSafeLoader"] ["yaml", "SafeLoader" | "CSafeLoader"]
| ["yaml", "loader", "SafeLoader" | "CSafeLoader"] | ["yaml", "loader", "SafeLoader" | "CSafeLoader"]
| ["yaml", "cyaml", "CSafeLoader"]
) )
}) })
{ {

View File

@ -254,84 +254,3 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
42 | urllib.request.urlopen(urllib.request.Request(url)) 42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:51:1
|
49 | # https://github.com/astral-sh/ruff/issues/21462
50 | path = "https://example.com/data.csv"
51 | urllib.request.urlretrieve(path, "data.csv")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:53:1
|
51 | urllib.request.urlretrieve(path, "data.csv")
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 |
55 | # Test resolved f-strings and concatenated string literals
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:57:1
|
55 | # Test resolved f-strings and concatenated string literals
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:58:1
|
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
58 | urllib.request.Request(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | concatenated_url = "https://" + "example.com/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:61:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | urllib.request.Request(concatenated_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:62:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
62 | urllib.request.Request(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:65:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | urllib.request.Request(nested_concatenated)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:66:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
66 | urllib.request.Request(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@ -1,15 +1,15 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
-linter.preview = disabled --> S301.py:3:1
+linter.preview = enabled |
1 | import pickle
2 |
3 | pickle.loads()
| ^^^^^^^^^^^^^^
|
--- Summary ---
Removed: 0
Added: 2
--- Added ---
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:7:5 --> S301.py:7:5
| |
@ -19,7 +19,6 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un
8 | foo = pickle.load 8 | foo = pickle.load
| |
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:8:7 --> S301.py:8:7
| |

View File

@ -1,15 +1,24 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S307 Use of possibly insecure function; consider using `ast.literal_eval`
-linter.preview = disabled --> S307.py:3:7
+linter.preview = enabled |
1 | import os
2 |
3 | print(eval("1+1")) # S307
| ^^^^^^^^^^^
4 | print(eval("os.getcwd()")) # S307
|
--- Summary --- S307 Use of possibly insecure function; consider using `ast.literal_eval`
Removed: 0 --> S307.py:4:7
Added: 2 |
3 | print(eval("1+1")) # S307
4 | print(eval("os.getcwd()")) # S307
| ^^^^^^^^^^^^^^^^^^^
|
--- Added ---
S307 Use of possibly insecure function; consider using `ast.literal_eval` S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:16:5 --> S307.py:16:5
| |
@ -19,7 +28,6 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval`
17 | foo = eval 17 | foo = eval
| |
S307 Use of possibly insecure function; consider using `ast.literal_eval` S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:17:7 --> S307.py:17:7
| |

View File

@ -1,37 +1,60 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 2
Added: 4
--- Removed ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:16:1 --> S308.py:6:5
| |
16 | @mark_safe 4 | def bad_func():
| ^^^^^^^^^^ 5 | inject = "harmful_input"
17 | def some_func(): 6 | mark_safe(inject)
18 | return '<script>alert("evil!")</script>' | ^^^^^^^^^^^^^^^^^
| 7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:36:1 --> S308.py:7:5
|
5 | inject = "harmful_input"
6 | mark_safe(inject)
7 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:8:5
| |
36 | @mark_safe 6 | mark_safe(inject)
| ^^^^^^^^^^ 7 | mark_safe("I will add" + inject + "to my string")
37 | def some_func(): 8 | mark_safe("I will add %s to my string" % inject)
38 | return '<script>alert("evil!")</script>' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
| |
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:9:5
|
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:10:5
|
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | def good_func():
|
--- Added ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:16:2 --> S308.py:16:2
| |
@ -41,6 +64,59 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
18 | return '<script>alert("evil!")</script>' 18 | return '<script>alert("evil!")</script>'
| |
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:26:5
|
24 | def bad_func():
25 | inject = "harmful_input"
26 | mark_safe(inject)
| ^^^^^^^^^^^^^^^^^
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:27:5
|
25 | inject = "harmful_input"
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:28:5
|
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:29:5
|
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:30:5
|
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 |
32 | def good_func():
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:36:2 --> S308.py:36:2
@ -51,7 +127,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
38 | return '<script>alert("evil!")</script>' 38 | return '<script>alert("evil!")</script>'
| |
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:42:5 --> S308.py:42:5
| |
@ -61,7 +136,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
43 | foo = mark_safe 43 | foo = mark_safe
| |
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:43:7 --> S308.py:43:7
| |

View File

@ -1,106 +1,260 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 8
Added: 2
--- Removed ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:51:1 --> S310.py:6:1
| |
49 | # https://github.com/astral-sh/ruff/issues/21462 4 | urllib.request.urlopen(url=f'http://www.google.com')
50 | path = "https://example.com/data.csv" 5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
51 | urllib.request.urlretrieve(path, "data.csv") 6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api" 7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
53 | urllib.request.Request(url) 8 | urllib.request.urlopen('http://www.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:53:1 --> S310.py:7:1
|
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | urllib.request.urlopen('http://www.google.com')
9 | urllib.request.urlopen(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:10:1
| |
51 | urllib.request.urlretrieve(path, "data.csv") 8 | urllib.request.urlopen('http://www.google.com')
52 | url = "https://example.com/api" 9 | urllib.request.urlopen(f'http://www.google.com')
53 | urllib.request.Request(url) 10 | urllib.request.urlopen('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | urllib.request.urlopen(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:11:1
|
9 | urllib.request.urlopen(f'http://www.google.com')
10 | urllib.request.urlopen('file:///foo/bar/baz')
11 | urllib.request.urlopen(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 | 12 |
55 | # Test resolved f-strings and concatenated string literals 13 | urllib.request.Request(url='http://www.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:57:1 --> S310.py:16:1
| |
55 | # Test resolved f-strings and concatenated string literals 14 | urllib.request.Request(url=f'http://www.google.com')
56 | fstring_url = f"https://example.com/data.csv" 15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
57 | urllib.request.urlopen(fstring_url) 16 | urllib.request.Request(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url) 17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
18 | urllib.request.Request('http://www.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:58:1 --> S310.py:17:1
| |
56 | fstring_url = f"https://example.com/data.csv" 15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
57 | urllib.request.urlopen(fstring_url) 16 | urllib.request.Request(url='http://www.google.com', **kwargs)
58 | urllib.request.Request(fstring_url) 17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 | 18 | urllib.request.Request('http://www.google.com')
60 | concatenated_url = "https://" + "example.com/data.csv" 19 | urllib.request.Request(f'http://www.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:61:1 --> S310.py:20:1
| |
60 | concatenated_url = "https://" + "example.com/data.csv" 18 | urllib.request.Request('http://www.google.com')
61 | urllib.request.urlopen(concatenated_url) 19 | urllib.request.Request(f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 20 | urllib.request.Request('file:///foo/bar/baz')
62 | urllib.request.Request(concatenated_url) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | urllib.request.Request(url)
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:62:1 --> S310.py:21:1
| |
60 | concatenated_url = "https://" + "example.com/data.csv" 19 | urllib.request.Request(f'http://www.google.com')
61 | urllib.request.urlopen(concatenated_url) 20 | urllib.request.Request('file:///foo/bar/baz')
62 | urllib.request.Request(concatenated_url) 21 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 | 22 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv" 23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:65:1 --> S310.py:23:1
| |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv" 21 | urllib.request.Request(url)
65 | urllib.request.urlopen(nested_concatenated) 22 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
66 | urllib.request.Request(nested_concatenated) | ^^^^^^^^^^^^^^^^^^^^^^^^^^
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:66:1 --> S310.py:24:1
| |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv" 23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
65 | urllib.request.urlopen(nested_concatenated) 24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
66 | urllib.request.Request(nested_concatenated) | ^^^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:25:1
|
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:26:1
|
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:27:1
|
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:28:1
|
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:29:1
|
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:30:1
|
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:31:1
|
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:32:1
|
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
33 |
34 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:37:1
|
35 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:38:1
|
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:1
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:24
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:1
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:24
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
--- Added ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:46:5 --> S310.py:46:5
| |
@ -110,7 +264,6 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
47 | foo = urllib.request.urlopen 47 | foo = urllib.request.urlopen
| |
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:47:7 --> S310.py:47:7
| |
@ -118,6 +271,4 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
46 | map(urllib.request.urlopen, []) 46 | map(urllib.request.urlopen, [])
47 | foo = urllib.request.urlopen 47 | foo = urllib.request.urlopen
| ^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^
48 |
49 | # https://github.com/astral-sh/ruff/issues/21462
| |

View File

@ -1,15 +1,103 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S311 Standard pseudo-random generators are not suitable for cryptographic purposes
-linter.preview = disabled --> S311.py:10:1
+linter.preview = enabled |
9 | # Errors
10 | random.Random()
| ^^^^^^^^^^^^^^^
11 | random.random()
12 | random.randrange()
|
--- Summary --- S311 Standard pseudo-random generators are not suitable for cryptographic purposes
Removed: 0 --> S311.py:11:1
Added: 2 |
9 | # Errors
10 | random.Random()
11 | random.random()
| ^^^^^^^^^^^^^^^
12 | random.randrange()
13 | random.randint()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:12:1
|
10 | random.Random()
11 | random.random()
12 | random.randrange()
| ^^^^^^^^^^^^^^^^^^
13 | random.randint()
14 | random.choice()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:13:1
|
11 | random.random()
12 | random.randrange()
13 | random.randint()
| ^^^^^^^^^^^^^^^^
14 | random.choice()
15 | random.choices()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:14:1
|
12 | random.randrange()
13 | random.randint()
14 | random.choice()
| ^^^^^^^^^^^^^^^
15 | random.choices()
16 | random.uniform()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:15:1
|
13 | random.randint()
14 | random.choice()
15 | random.choices()
| ^^^^^^^^^^^^^^^^
16 | random.uniform()
17 | random.triangular()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:16:1
|
14 | random.choice()
15 | random.choices()
16 | random.uniform()
| ^^^^^^^^^^^^^^^^
17 | random.triangular()
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:17:1
|
15 | random.choices()
16 | random.uniform()
17 | random.triangular()
| ^^^^^^^^^^^^^^^^^^^
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:18:1
|
16 | random.uniform()
17 | random.triangular()
18 | random.randbytes()
| ^^^^^^^^^^^^^^^^^^
19 |
20 | # Unrelated
|
--- Added ---
S311 Standard pseudo-random generators are not suitable for cryptographic purposes S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:26:5 --> S311.py:26:5
| |
@ -19,7 +107,6 @@ S311 Standard pseudo-random generators are not suitable for cryptographic purpos
27 | foo = random.randrange 27 | foo = random.randrange
| |
S311 Standard pseudo-random generators are not suitable for cryptographic purposes S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:27:7 --> S311.py:27:7
| |

View File

@ -1,15 +1,15 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
-linter.preview = disabled --> S312.py:3:1
+linter.preview = enabled |
1 | from telnetlib import Telnet
2 |
3 | Telnet("localhost", 23)
| ^^^^^^^^^^^^^^^^^^^^^^^
|
--- Summary ---
Removed: 0
Added: 3
--- Added ---
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol. S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:7:5 --> S312.py:7:5
| |
@ -19,7 +19,6 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
8 | foo = Telnet 8 | foo = Telnet
| |
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol. S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:8:7 --> S312.py:8:7
| |
@ -31,7 +30,6 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
10 | import telnetlib 10 | import telnetlib
| |
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol. S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:11:5 --> S312.py:11:5
| |
@ -41,3 +39,13 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
12 | 12 |
13 | from typing import Annotated 13 | from typing import Annotated
| |
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:14:24
|
13 | from typing import Annotated
14 | foo: Annotated[Telnet, telnetlib.Telnet()]
| ^^^^^^^^^^^^^^^^^^
15 |
16 | def _() -> Telnet: ...
|

View File

@ -1,15 +1,26 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
-linter.preview = disabled --> S508.py:3:25
+linter.preview = enabled |
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
4 | CommunityData("public", mpModel=1) # S508
|
--- Summary --- S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
Removed: 0 --> S508.py:4:25
Added: 8 |
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^^^^^^^^^
5 |
6 | CommunityData("public", mpModel=2) # OK
|
--- Added ---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:18:46 --> S508.py:18:46
| |
@ -21,7 +32,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:19:58 --> S508.py:19:58
| |
@ -32,7 +42,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:20:53 --> S508.py:20:53
| |
@ -44,7 +53,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:21:45 --> S508.py:21:45
| |
@ -56,7 +64,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:22:58 --> S508.py:22:58
| |
@ -68,7 +75,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:23:53 --> S508.py:23:53
| |
@ -80,7 +86,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:24:45 --> S508.py:24:45
| |
@ -91,7 +96,6 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
| |
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:25:43 --> S508.py:25:43
| |

View File

@ -1,15 +1,24 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
--- ---
--- Linter settings --- S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
-linter.preview = disabled --> S509.py:4:12
+linter.preview = enabled |
4 | insecure = UsmUserData("securityName") # S509
| ^^^^^^^^^^^
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
--- Summary --- S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
Removed: 0 --> S509.py:5:16
Added: 4 |
4 | insecure = UsmUserData("securityName") # S509
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
| ^^^^^^^^^^^
6 |
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
--- Added ---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:15:1 --> S509.py:15:1
| |
@ -21,7 +30,6 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
| |
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:16:1 --> S509.py:16:1
| |
@ -32,7 +40,6 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
| |
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:17:1 --> S509.py:17:1
| |
@ -43,7 +50,6 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
| |
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:18:1 --> S509.py:18:1
| |

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