diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 43e454d164..ac3adb1572 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -19,6 +19,10 @@
# ty
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
-/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
+/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
+/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
+/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
+/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
+/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml
index d7ba26ba46..c26802228f 100644
--- a/.github/workflows/build-binaries.yml
+++ b/.github/workflows/build-binaries.yml
@@ -39,17 +39,17 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
command: sdist
args: --out dist
@@ -68,18 +68,18 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: x86_64
args: --release --locked --out dist
@@ -110,18 +110,18 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: aarch64
args: --release --locked --out dist
@@ -166,18 +166,18 @@ jobs:
- target: aarch64-pc-windows-msvc
arch: x64
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -219,18 +219,18 @@ jobs:
- x86_64-unknown-linux-gnu
- i686-unknown-linux-gnu
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -292,19 +292,21 @@ jobs:
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: arm-unknown-linux-musleabihf
arch: arm
+ - target: riscv64gc-unknown-linux-gnu
+ arch: riscv64
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -319,7 +321,7 @@ jobs:
githubToken: ${{ github.token }}
install: |
apt-get update
- apt-get install -y --no-install-recommends python3 python3-pip
+ apt-get install -y --no-install-recommends python3 python3-pip libatomic1
pip3 install -U pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
@@ -359,18 +361,18 @@ jobs:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -425,17 +427,17 @@ jobs:
arch: armv7
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml
index 7e2c1c5c2f..70275ab152 100644
--- a/.github/workflows/build-docker.yml
+++ b/.github/workflows/build-docker.yml
@@ -33,14 +33,14 @@ jobs:
- linux/amd64
- linux/arm64
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
persist-credentials: false
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
@@ -113,7 +113,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
path: /tmp/digests
pattern: digests-*
@@ -123,7 +123,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
@@ -131,7 +131,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -169,7 +169,7 @@ jobs:
steps:
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -256,7 +256,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
path: /tmp/digests
pattern: digests-*
@@ -266,7 +266,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
@@ -276,7 +276,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 21242f7e6e..9091ae3907 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -38,11 +38,12 @@ jobs:
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
# Flag that is set to "true" when code related to ty changes.
ty: ${{ steps.check_ty.outputs.changed }}
-
+ # Flag that is set to "true" when code related to the py-fuzzer folder changes.
+ py-fuzzer: ${{ steps.check_py_fuzzer.outputs.changed }}
# Flag that is set to "true" when code related to the playground changes.
playground: ${{ steps.check_playground.outputs.changed }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -68,7 +69,6 @@ jobs:
':crates/ruff_text_size/**' \
':crates/ruff_python_ast/**' \
':crates/ruff_python_parser/**' \
- ':python/py-fuzzer/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
@@ -88,7 +88,6 @@ jobs:
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
- ':!crates/ruff_db/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
@@ -138,17 +137,29 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
+ - name: Check if the py-fuzzer code changed
+ id: check_py_fuzzer
+ env:
+ MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
+ run: |
+ if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py_fuzzer/**' \
+ ; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ fi
+
- name: Check if there was any code related change
id: check_code
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
- if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
- ':!**/*.md' \
- ':crates/ty_python_semantic/resources/mdtest/**/*.md' \
+ # NOTE: Do not exclude all Markdown files here, but rather use
+ # specific exclude patterns like 'docs/**'), because tests for
+ # 'ty' are written in Markdown.
+ if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':!docs/**' \
':!assets/**' \
- ':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
@@ -197,7 +208,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Install Rust toolchain"
@@ -211,10 +222,10 @@ jobs:
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -231,22 +242,26 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
- uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
+ uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
+ - name: "Install uv"
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ with:
+ enable-cache: "true"
- name: ty mdtests (GitHub annotations)
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
env:
@@ -289,22 +304,26 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
- uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
+ uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
+ - name: "Install uv"
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ with:
+ enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -318,16 +337,20 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
+ - name: "Install uv"
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ with:
+ enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -345,15 +368,15 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
- node-version: 20
+ node-version: 22
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
@@ -374,14 +397,14 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
- uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
+ uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build"
run: cargo build --release --locked
@@ -392,7 +415,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0
@@ -400,27 +423,18 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
- uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- - name: "Install cargo nextest"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
- with:
- tool: cargo-nextest
- - name: "Install cargo insta"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
- with:
- tool: cargo-insta
- - name: "Run tests"
+ uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
+ - name: "Build tests"
shell: bash
env:
- NEXTEST_PROFILE: "ci"
MSRV: ${{ steps.msrv.outputs.value }}
- run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
+ run: cargo "+${MSRV}" test --no-run --all-features
cargo-fuzz-build:
name: "cargo fuzz build"
@@ -429,18 +443,16 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 10
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
- with:
- tool: cargo-fuzz@0.11.2
+ uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
- name: "Install cargo-fuzz"
# 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
@@ -452,16 +464,16 @@ jobs:
needs:
- cargo-test-linux
- determine_changes
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && needs.determine_changes.outputs.parser == 'true' }}
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.parser == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: 20
env:
FORCE_COLOR: 1
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download Ruff binary to test
id: download-cached-binary
with:
@@ -491,10 +503,10 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 5
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -521,14 +533,14 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -642,13 +654,13 @@ jobs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.ty == 'true' }}
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download new ty binary
id: ty-new
with:
@@ -661,7 +673,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -688,31 +700,49 @@ jobs:
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
+ - uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
+ ty-completion-evaluation:
+ name: "ty completion evaluation"
+ runs-on: depot-ubuntu-22.04-16
+ needs: determine_changes
+ if: ${{ needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main' }}
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ persist-credentials: false
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - name: "Install Rust toolchain"
+ run: rustup show
+ - name: "Run ty completion evaluation"
+ run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
+ - name: "Ensure there are no changes"
+ run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
+
python-package:
name: "python package"
runs-on: ubuntu-latest
timeout-minutes: 20
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
- uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
args: --out dist
- name: "Test wheel"
@@ -728,16 +758,16 @@ jobs:
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 10
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
- name: "Cache pre-commit"
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -759,13 +789,13 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.13"
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
@@ -774,7 +804,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -801,10 +831,10 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Run checks"
@@ -827,18 +857,18 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
name: "Download ruff-lsp source"
with:
persist-credentials: false
repository: "astral-sh/ruff-lsp"
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
# installation fails on 3.13 and newer
python-version: "3.12"
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download development ruff binary
id: ruff-target
with:
@@ -869,13 +899,13 @@ jobs:
- determine_changes
if: ${{ (needs.determine_changes.outputs.playground == 'true') }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm"
@@ -894,58 +924,101 @@ jobs:
run: npm run fmt:check
working-directory: playground
- benchmarks-instrumented:
+ benchmarks-instrumented-ruff:
+ name: "benchmarks instrumented (ruff)"
runs-on: ubuntu-24.04
needs: determine_changes
- if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
+ if: |
+ github.ref == 'refs/heads/main' ||
+ (needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-codspeed
- name: "Build benchmarks"
- run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
+ run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
- uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
+ uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
with:
+ mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
- benchmarks-walltime:
- runs-on: codspeed-macro
+ benchmarks-instrumented-ty:
+ name: "benchmarks instrumented (ty)"
+ runs-on: ubuntu-24.04
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.ref == 'refs/heads/main' ||
+ needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
- env:
- TY_LOG: ruff_benchmark=debug
steps:
- name: "Checkout Branch"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
- uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
+ with:
+ tool: cargo-codspeed
+
+ - name: "Build benchmarks"
+ run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty
+
+ - name: "Run benchmarks"
+ uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
+ with:
+ mode: instrumentation
+ run: cargo codspeed run
+ token: ${{ secrets.CODSPEED_TOKEN }}
+
+ benchmarks-walltime:
+ name: "benchmarks walltime (${{ matrix.benchmarks }})"
+ runs-on: codspeed-macro
+ needs: determine_changes
+ if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
+ timeout-minutes: 20
+ strategy:
+ matrix:
+ benchmarks:
+ - "medium|multithreaded"
+ - "small|large"
+ steps:
+ - name: "Checkout Branch"
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ persist-credentials: false
+
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+
+ - name: "Install Rust toolchain"
+ run: rustup show
+
+ - name: "Install codspeed"
+ uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-codspeed
@@ -953,7 +1026,13 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
- uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
+ uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
+ env:
+ # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
+ # appear to provide much useful insight for our walltime benchmarks right now
+ # (see https://github.com/astral-sh/ruff/pull/20419)
+ CODSPEED_PERF_ENABLED: false
with:
- run: cargo codspeed run
+ mode: walltime
+ run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
token: ${{ secrets.CODSPEED_TOKEN }}
diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml
index 5ae45c5e7d..2f54dd1082 100644
--- a/.github/workflows/daily_fuzz.yaml
+++ b/.github/workflows/daily_fuzz.yaml
@@ -31,15 +31,15 @@ jobs:
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
- uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: Build ruff
# A debug build means the script runs slower once it gets started,
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
@@ -65,7 +65,7 @@ jobs:
permissions:
issues: write
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml
index 1da799ff3e..f3f6596066 100644
--- a/.github/workflows/mypy_primer.yaml
+++ b/.github/workflows/mypy_primer.yaml
@@ -11,7 +11,9 @@ on:
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
+ - "scripts/mypy_primer.sh"
- "Cargo.lock"
+ - "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
@@ -30,16 +32,16 @@ jobs:
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - 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@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
workspaces: "ruff"
@@ -49,46 +51,12 @@ jobs:
- name: Run mypy_primer
shell: bash
env:
- TY_MEMORY_REPORT: mypy_primer
+ PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
+ DIFF_FILE: mypy_primer.diff
run: |
cd ruff
-
- echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
- mkdir -p ~/.config/ty
- cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
-
- PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
-
- echo "new commit"
- git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
-
- MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
- git checkout -b base_commit "$MERGE_BASE"
- echo "base commit"
- git rev-list --format=%s --max-count=1 base_commit
-
- cd ..
-
- echo "Project selector: $PRIMER_SELECTOR"
- # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
- uvx \
- --from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
- mypy_primer \
- --repo ruff \
- --type-checker ty \
- --old base_commit \
- --new "$GITHUB_SHA" \
- --project-selector "/($PRIMER_SELECTOR)\$" \
- --output concise \
- --debug > mypy_primer.diff || [ $? -eq 1 ]
-
- # Output diff with ANSI color codes
- cat mypy_primer.diff
-
- # Remove ANSI color codes before uploading
- sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
-
- echo ${{ github.event.number }} > pr-number
+ scripts/mypy_primer.sh
+ echo ${{ github.event.number }} > ../pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -101,3 +69,41 @@ jobs:
with:
name: pr-number
path: pr-number
+
+ memory_usage:
+ name: Run memory statistics
+ runs-on: depot-ubuntu-22.04-32
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ with:
+ workspaces: "ruff"
+
+ - name: Install Rust toolchain
+ run: rustup show
+
+ - name: Run mypy_primer
+ shell: bash
+ env:
+ TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
+ TY_MEMORY_REPORT: mypy_primer
+ PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/memory.txt
+ DIFF_FILE: mypy_primer_memory.diff
+ run: |
+ cd ruff
+ scripts/mypy_primer.sh
+
+ - name: Upload diff
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mypy_primer_memory_diff
+ path: mypy_primer_memory.diff
diff --git a/.github/workflows/mypy_primer_comment.yaml b/.github/workflows/mypy_primer_comment.yaml
index 85c3367780..895956e766 100644
--- a/.github/workflows/mypy_primer_comment.yaml
+++ b/.github/workflows/mypy_primer_comment.yaml
@@ -45,15 +45,28 @@ jobs:
if_no_artifact_found: ignore
allow_forks: true
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: "Download mypy_primer memory results"
+ id: download-mypy_primer_memory_diff
+ if: steps.pr-number.outputs.pr-number
+ with:
+ name: mypy_primer_memory_diff
+ workflow: mypy_primer.yaml
+ pr: ${{ steps.pr-number.outputs.pr-number }}
+ path: pr/mypy_primer_memory_diff
+ workflow_conclusion: completed
+ if_no_artifact_found: ignore
+ allow_forks: true
+
- name: Generate comment content
id: generate-comment
- if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
+ if: ${{ steps.download-mypy_primer_diff.outputs.found_artifact == 'true' && steps.download-mypy_primer_memory_diff.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious mypy_primer results that symlink to a secret
# file on this runner
- if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
+ if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] || [[ -L pr/mypy_primer_memory_diff/mypy_primer_memory.diff ]]
then
- echo "Error: mypy_primer.diff cannot be a symlink"
+ echo "Error: mypy_primer.diff and mypy_primer_memory.diff cannot be a symlink"
exit 1
fi
@@ -74,6 +87,18 @@ jobs:
echo 'No ecosystem changes detected ✅' >> comment.txt
fi
+ if [ -s "pr/mypy_primer_memory_diff/mypy_primer_memory.diff" ]; then
+ echo '' >> comment.txt
+ echo 'Memory usage changes were detected when running on open source projects
' >> comment.txt
+ echo '' >> comment.txt
+ echo '```diff' >> comment.txt
+ cat pr/mypy_primer_memory_diff/mypy_primer_memory.diff >> comment.txt
+ echo '```' >> comment.txt
+ echo ' ' >> comment.txt
+ else
+ echo 'No memory usage changes detected ✅' >> comment.txt
+ fi
+
echo 'comment<> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/notify-dependents.yml b/.github/workflows/notify-dependents.yml
index 3f5da37782..c7f7224d7a 100644
--- a/.github/workflows/notify-dependents.yml
+++ b/.github/workflows/notify-dependents.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Update pre-commit mirror"
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index b1c819f5dc..8adbe4536c 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -23,12 +23,12 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ inputs.ref }}
persist-credentials: true
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: 3.12
@@ -68,7 +68,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml
index e7bf15dde2..d40850afeb 100644
--- a/.github/workflows/publish-playground.yml
+++ b/.github/workflows/publish-playground.yml
@@ -24,12 +24,12 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm"
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index c8ed0be0e4..e5473f80a3 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -22,8 +22,8 @@ jobs:
id-token: write
steps:
- name: "Install uv"
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: wheels-*
path: wheels
diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml
index b5dc37dc47..e842ab6928 100644
--- a/.github/workflows/publish-ty-playground.yml
+++ b/.github/workflows/publish-ty-playground.yml
@@ -30,12 +30,12 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml
index 81193b9352..a51c888286 100644
--- a/.github/workflows/publish-wasm.yml
+++ b/.github/workflows/publish-wasm.yml
@@ -29,7 +29,7 @@ jobs:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Install Rust toolchain"
@@ -45,9 +45,9 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
- node-version: 20
+ node-version: 22
registry-url: "https://registry.npmjs.org"
- name: "Publish (dry-run)"
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 88048a0bcc..b53ce5a2d0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
+ - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -124,19 +124,19 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
+ - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: target/distrib/
@@ -175,19 +175,19 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
+ - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: target/distrib/
@@ -251,13 +251,13 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
+ - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: artifacts
diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml
index c384d8868a..2185d474d1 100644
--- a/.github/workflows/sync_typeshed.yaml
+++ b/.github/workflows/sync_typeshed.yaml
@@ -1,5 +1,25 @@
name: Sync typeshed
+# How this works:
+#
+# 1. A Linux worker:
+# a. Checks out Ruff and typeshed
+# b. Deletes the vendored typeshed stdlib stubs from Ruff
+# c. Copies the latest versions of the stubs from typeshed
+# d. Uses docstring-adder to sync all docstrings available on Linux
+# e. Creates a new branch on the upstream astral-sh/ruff repository
+# f. Commits the changes it's made and pushes them to the new upstream branch
+# 2. Once the Linux worker is done, a Windows worker:
+# a. Checks out the branch created by the Linux worker
+# b. Syncs all docstrings available on Windows that are not available on Linux
+# c. Commits the changes and pushes them to the same upstream branch
+# 3. Once the Windows worker is done, a MacOS worker:
+# a. Checks out the branch created by the Linux worker
+# b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
+# c. Commits the changes and pushes them to the same upstream branch
+# d. Creates a PR against the `main` branch using the branch all three workers have pushed to
+# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository
+
on:
workflow_dispatch:
schedule:
@@ -10,7 +30,17 @@ env:
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
+ # The name of the upstream branch that the first worker creates,
+ # and which all three workers push to.
+ UPSTREAM_BRANCH: typeshedbot/sync-typeshed
+
+ # The path to the directory that contains the vendored typeshed stubs,
+ # relative to the root of the Ruff repository.
+ VENDORED_TYPESHED: crates/ty_vendored/vendor/typeshed
+
jobs:
+ # Sync typeshed stubs, and sync all docstrings available on Linux.
+ # Push the changes to a new branch on the upstream repository.
sync:
name: Sync typeshed
runs-on: ubuntu-latest
@@ -19,14 +49,13 @@ jobs:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
permissions:
contents: write
- pull-requests: write
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
name: Checkout Ruff
with:
path: ruff
persist-credentials: true
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
name: Checkout typeshed
with:
repository: python/typeshed
@@ -36,41 +65,134 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- - name: Sync typeshed
- id: sync
- run: |
- rm -rf ruff/crates/ty_vendored/vendor/typeshed
- mkdir ruff/crates/ty_vendored/vendor/typeshed
- cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
- cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed
- cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
- rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
- git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
- - name: Commit the changes
- id: commit
- if: ${{ steps.sync.outcome == 'success' }}
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - name: Sync typeshed stubs
run: |
+ rm -rf "ruff/${VENDORED_TYPESHED}"
+ mkdir "ruff/${VENDORED_TYPESHED}"
+ cp typeshed/README.md "ruff/${VENDORED_TYPESHED}"
+ cp typeshed/LICENSE "ruff/${VENDORED_TYPESHED}"
+
+ # The pyproject.toml file is needed by a later job for the black configuration.
+ # It's deleted before creating the PR.
+ cp typeshed/pyproject.toml "ruff/${VENDORED_TYPESHED}"
+
+ cp -r typeshed/stdlib "ruff/${VENDORED_TYPESHED}/stdlib"
+ rm -rf "ruff/${VENDORED_TYPESHED}/stdlib/@tests"
+ git -C typeshed rev-parse HEAD > "ruff/${VENDORED_TYPESHED}/source_commit.txt"
cd ruff
- git checkout -b typeshedbot/sync-typeshed
+ git checkout -b "${UPSTREAM_BRANCH}"
git add .
- git diff --staged --quiet || git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)"
- - name: Create a PR
- if: ${{ steps.sync.outcome == 'success' && steps.commit.outcome == 'success' }}
+ git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
+ - name: Sync Linux docstrings
+ if: ${{ success() }}
run: |
cd ruff
- git push --force origin typeshedbot/sync-typeshed
- gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
+ ./scripts/codemod_docstrings.sh
+ git commit -am "Sync Linux docstrings" --allow-empty
+ - name: Push the changes
+ id: commit
+ if: ${{ success() }}
+ run: git -C ruff push --force --set-upstream origin "${UPSTREAM_BRANCH}"
+
+ # Checkout the branch created by the sync job,
+ # and sync all docstrings available on Windows that are not available on Linux.
+ # Commit the changes and push them to the same branch.
+ docstrings-windows:
+ runs-on: windows-latest
+ timeout-minutes: 20
+ needs: [sync]
+
+ # Don't run the cron job on forks.
+ # The job will also be skipped if the sync job failed, because it's specified in `needs` above,
+ # and we haven't used `always()` in the `if` condition here
+ # (https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-requiring-successful-dependent-jobs)
+ if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
+
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ name: Checkout Ruff
+ with:
+ persist-credentials: true
+ ref: ${{ env.UPSTREAM_BRANCH}}
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - name: Setup git
+ run: |
+ git config --global user.name typeshedbot
+ git config --global user.email '<>'
+ - name: Sync Windows docstrings
+ id: docstrings
+ shell: bash
+ run: ./scripts/codemod_docstrings.sh
+ - name: Commit the changes
+ if: ${{ steps.docstrings.outcome == 'success' }}
+ run: |
+ git commit -am "Sync Windows docstrings" --allow-empty
+ git push
+
+ # Checkout the branch created by the sync job,
+ # and sync all docstrings available on macOS that are not available on Linux or Windows.
+ # Push the changes to the same branch and create a PR against the `main` branch using that branch.
+ docstrings-macos-and-pr:
+ runs-on: macos-latest
+ timeout-minutes: 20
+ needs: [sync, docstrings-windows]
+
+ # Don't run the cron job on forks.
+ # The job will also be skipped if the sync or docstrings-windows jobs failed,
+ # because they're specified in `needs` above and we haven't used an `always()` condition in the `if` here
+ # (https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-requiring-successful-dependent-jobs)
+ if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
+
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ name: Checkout Ruff
+ with:
+ persist-credentials: true
+ ref: ${{ env.UPSTREAM_BRANCH}}
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ - name: Setup git
+ run: |
+ git config --global user.name typeshedbot
+ git config --global user.email '<>'
+ - name: Sync macOS docstrings
+ run: ./scripts/codemod_docstrings.sh
+ - name: Commit and push the changes
+ if: ${{ success() }}
+ run: |
+ git commit -am "Sync macOS docstrings" --allow-empty
+
+ # Here we just reformat the codemodded stubs so that they are
+ # consistent with the other typeshed stubs around them.
+ # Typeshed formats code using black in their CI, so we just invoke
+ # black on the stubs the same way that typeshed does.
+ uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
+ git commit -am "Format codemodded docstrings" --allow-empty
+
+ rm "${VENDORED_TYPESHED}/pyproject.toml"
+ git commit -am "Remove pyproject.toml file"
+
+ git push
+ - name: Create a PR
+ if: ${{ success() }}
+ run: |
+ gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
create-issue-on-failure:
name: Create an issue if the typeshed sync failed
runs-on: ubuntu-latest
- needs: [sync]
- if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.sync.result == 'failure' }}
+ needs: [sync, docstrings-windows, docstrings-macos-and-pr]
+ if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && (needs.sync.result == 'failure' || needs.docstrings-windows.result == 'failure' || needs.docstrings-macos-and-pr.result == 'failure') }}
permissions:
issues: write
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml
index cb3177f863..ae847d1684 100644
--- a/.github/workflows/ty-ecosystem-analyzer.yaml
+++ b/.github/workflows/ty-ecosystem-analyzer.yaml
@@ -17,6 +17,7 @@ env:
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
REF_NAME: ${{ github.ref_name }}
+ CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-analyzer:
@@ -25,16 +26,16 @@ jobs:
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - 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@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
workspaces: "ruff"
@@ -63,32 +64,90 @@ jobs:
cd ..
- uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@9c34dc514ee9aef6735db1dfebb80f63acbc3440"
+ uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
ecosystem-analyzer \
--repository ruff \
- analyze \
- --projects ruff/projects_old.txt \
- --commit old_commit \
- --output diagnostics_old.json
+ diff \
+ --profile=release \
+ --projects-old ruff/projects_old.txt \
+ --projects-new ruff/projects_new.txt \
+ --old old_commit \
+ --new new_commit \
+ --output-old diagnostics-old.json \
+ --output-new diagnostics-new.json
- ecosystem-analyzer \
- --repository ruff \
- analyze \
- --projects ruff/projects_new.txt \
- --commit new_commit \
- --output diagnostics_new.json
+ mkdir dist
ecosystem-analyzer \
generate-diff \
- diagnostics_old.json \
- diagnostics_new.json \
+ diagnostics-old.json \
+ diagnostics-new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
- --output-html diff.html
+ --output-html dist/diff.html
- - name: Upload HTML diff report
+ ecosystem-analyzer \
+ generate-diff-statistics \
+ diagnostics-old.json \
+ diagnostics-new.json \
+ --old-name "main (merge base)" \
+ --new-name "$REF_NAME" \
+ --output diff-statistics.md
+
+ ecosystem-analyzer \
+ generate-timing-diff \
+ diagnostics-old.json \
+ diagnostics-new.json \
+ --old-name "main (merge base)" \
+ --new-name "$REF_NAME" \
+ --output-html dist/timing.html
+
+ echo '## `ecosystem-analyzer` results' > comment.md
+ echo >> comment.md
+ cat diff-statistics.md >> comment.md
+
+ cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY"
+
+ echo ${{ github.event.number }} > pr-number
+
+ - name: "Deploy to Cloudflare Pages"
+ if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
+ id: deploy
+ uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
+ with:
+ apiToken: ${{ secrets.CF_API_TOKEN }}
+ accountId: ${{ secrets.CF_ACCOUNT_ID }}
+ command: pages deploy dist --project-name=ty-ecosystem --branch ${{ github.head_ref }} --commit-hash ${GITHUB_SHA}
+
+ - name: "Append deployment URL"
+ if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
+ env:
+ DEPLOYMENT_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
+ run: |
+ echo >> comment.md
+ echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)** ([timing results]($DEPLOYMENT_URL/timing))" >> comment.md
+
+ - name: Upload comment
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: comment.md
+ path: comment.md
+
+ - name: Upload pr-number
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: pr-number
+ path: pr-number
+
+ - name: Upload diagnostics diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: diff.html
- path: diff.html
+ path: dist/diff.html
+
+ - name: Upload timing diff
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: timing.html
+ path: dist/timing.html
diff --git a/.github/workflows/ty-ecosystem-analyzer_comment.yaml b/.github/workflows/ty-ecosystem-analyzer_comment.yaml
new file mode 100644
index 0000000000..f237f45e1e
--- /dev/null
+++ b/.github/workflows/ty-ecosystem-analyzer_comment.yaml
@@ -0,0 +1,85 @@
+name: PR comment (ty ecosystem-analyzer)
+
+on: # zizmor: ignore[dangerous-triggers]
+ workflow_run:
+ workflows: [ty ecosystem-analyzer]
+ types: [completed]
+ workflow_dispatch:
+ inputs:
+ workflow_run_id:
+ description: The ty ecosystem-analyzer workflow that triggers the workflow run
+ required: true
+
+jobs:
+ comment:
+ runs-on: ubuntu-24.04
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: Download PR number
+ with:
+ name: pr-number
+ run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
+ if_no_artifact_found: ignore
+ allow_forks: true
+
+ - name: Parse pull request number
+ id: pr-number
+ run: |
+ if [[ -f pr-number ]]
+ then
+ echo "pr-number=$(> "$GITHUB_OUTPUT"
+ fi
+
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: "Download comment.md"
+ id: download-comment
+ if: steps.pr-number.outputs.pr-number
+ with:
+ name: comment.md
+ workflow: ty-ecosystem-analyzer.yaml
+ pr: ${{ steps.pr-number.outputs.pr-number }}
+ path: pr/comment
+ workflow_conclusion: completed
+ if_no_artifact_found: ignore
+ allow_forks: true
+
+ - name: Generate comment content
+ id: generate-comment
+ if: ${{ steps.download-comment.outputs.found_artifact == 'true' }}
+ run: |
+ # Guard against malicious ty ecosystem-analyzer results that symlink to a secret
+ # file on this runner
+ if [[ -L pr/comment/comment.md ]]
+ then
+ echo "Error: comment.md cannot be a symlink"
+ exit 1
+ fi
+
+ # Note: this identifier is used to find the comment to update on subsequent runs
+ echo '' > comment.md
+ echo >> comment.md
+ cat pr/comment/comment.md >> comment.md
+
+ echo 'comment<> "$GITHUB_OUTPUT"
+ cat comment.md >> "$GITHUB_OUTPUT"
+ echo 'EOF' >> "$GITHUB_OUTPUT"
+
+ - name: Find existing comment
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
+ if: steps.generate-comment.outcome == 'success'
+ id: find-comment
+ with:
+ issue-number: ${{ steps.pr-number.outputs.pr-number }}
+ comment-author: "github-actions[bot]"
+ body-includes: ""
+
+ - name: Create or update comment
+ if: steps.find-comment.outcome == 'success'
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
+ with:
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ issue-number: ${{ steps.pr-number.outputs.pr-number }}
+ body-path: comment.md
+ edit-mode: replace
diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml
new file mode 100644
index 0000000000..cf54bb4e1a
--- /dev/null
+++ b/.github/workflows/ty-ecosystem-report.yaml
@@ -0,0 +1,77 @@
+name: ty ecosystem-report
+
+permissions: {}
+
+on:
+ workflow_dispatch:
+ schedule:
+ # Run every Wednesday at 5:00 UTC:
+ - cron: 0 5 * * 3
+
+env:
+ CARGO_INCREMENTAL: 0
+ CARGO_NET_RETRY: 10
+ CARGO_TERM_COLOR: always
+ RUSTUP_MAX_RETRIES: 10
+ RUST_BACKTRACE: 1
+ CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
+
+jobs:
+ ty-ecosystem-report:
+ name: Create ecosystem report
+ runs-on: depot-ubuntu-22.04-32
+ timeout-minutes: 20
+ 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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ with:
+ workspaces: "ruff"
+
+ - name: Install Rust toolchain
+ run: rustup show
+
+ - name: Create report
+ shell: bash
+ run: |
+ cd ruff
+
+ echo "Enabling configuration overloads (see .github/mypy-primer-ty.toml)"
+ mkdir -p ~/.config/ty
+ cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
+
+ cd ..
+
+ uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
+
+ ecosystem-analyzer \
+ --verbose \
+ --repository ruff \
+ analyze \
+ --profile=release \
+ --projects ruff/crates/ty_python_semantic/resources/primer/good.txt \
+ --output ecosystem-diagnostics.json
+
+ mkdir dist
+
+ ecosystem-analyzer \
+ generate-report \
+ --max-diagnostics-per-project=1000 \
+ ecosystem-diagnostics.json \
+ --output dist/index.html
+
+ - name: "Deploy to Cloudflare Pages"
+ if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
+ id: deploy
+ uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
+ with:
+ apiToken: ${{ secrets.CF_API_TOKEN }}
+ accountId: ${{ secrets.CF_ACCOUNT_ID }}
+ command: pages deploy dist --project-name=ty-ecosystem --branch main --commit-hash ${GITHUB_SHA}
diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml
new file mode 100644
index 0000000000..5280236b4b
--- /dev/null
+++ b/.github/workflows/typing_conformance.yaml
@@ -0,0 +1,116 @@
+name: Run typing conformance
+
+permissions: {}
+
+on:
+ pull_request:
+ paths:
+ - "crates/ty*/**"
+ - "crates/ruff_db"
+ - "crates/ruff_python_ast"
+ - "crates/ruff_python_parser"
+ - ".github/workflows/typing_conformance.yaml"
+ - ".github/workflows/typing_conformance_comment.yaml"
+ - "Cargo.lock"
+ - "!**.md"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
+ cancel-in-progress: true
+
+env:
+ CARGO_INCREMENTAL: 0
+ CARGO_NET_RETRY: 10
+ CARGO_TERM_COLOR: always
+ RUSTUP_MAX_RETRIES: 10
+ RUST_BACKTRACE: 1
+ CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
+
+jobs:
+ typing_conformance:
+ name: Compute diagnostic diff
+ runs-on: depot-ubuntu-22.04-32
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ path: ruff
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ repository: python/typing
+ ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}
+ path: typing
+ persist-credentials: false
+
+ - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
+ with:
+ workspaces: "ruff"
+
+ - name: Install Rust toolchain
+ run: rustup show
+
+ - name: Compute diagnostic diff
+ shell: bash
+ env:
+ # TODO: Remove this once we fixed the remaining panics in the conformance suite.
+ TY_MAX_PARALLELISM: 1
+ run: |
+ RUFF_DIR="$GITHUB_WORKSPACE/ruff"
+
+ # Build the executable for the old and new commit
+ (
+ cd ruff
+
+ echo "new commit"
+ git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
+ cargo build --bin ty
+ mv target/debug/ty ty-new
+
+ MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
+ git checkout -b old_commit "$MERGE_BASE"
+ echo "old commit (merge base)"
+ git rev-list --format=%s --max-count=1 old_commit
+ cargo build --bin ty
+ mv target/debug/ty ty-old
+ )
+
+ (
+ cd typing/conformance/tests
+
+ echo "Running ty on old commit (merge base)"
+ "$RUFF_DIR/ty-old" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/old-output.txt" 2>&1 || true
+
+ echo "Running ty on new commit"
+ "$RUFF_DIR/ty-new" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/new-output.txt" 2>&1 || true
+ )
+
+ if ! diff -u old-output.txt new-output.txt > typing_conformance_diagnostics.diff; then
+ echo "Differences found between base and PR"
+ else
+ echo "No differences found"
+ touch typing_conformance_diagnostics.diff
+ fi
+
+ echo ${{ github.event.number }} > pr-number
+ echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
+
+ - name: Upload diff
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: typing_conformance_diagnostics_diff
+ path: typing_conformance_diagnostics.diff
+
+ - name: Upload pr-number
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: pr-number
+ path: pr-number
+
+ - name: Upload conformance suite commit
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: conformance-suite-commit
+ path: conformance-suite-commit
diff --git a/.github/workflows/typing_conformance_comment.yaml b/.github/workflows/typing_conformance_comment.yaml
new file mode 100644
index 0000000000..f596507448
--- /dev/null
+++ b/.github/workflows/typing_conformance_comment.yaml
@@ -0,0 +1,112 @@
+name: PR comment (typing_conformance)
+
+on: # zizmor: ignore[dangerous-triggers]
+ workflow_run:
+ workflows: [Run typing conformance]
+ types: [completed]
+ workflow_dispatch:
+ inputs:
+ workflow_run_id:
+ description: The typing_conformance workflow that triggers the workflow run
+ required: true
+
+jobs:
+ comment:
+ runs-on: ubuntu-24.04
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: Download PR number
+ with:
+ name: pr-number
+ run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
+ if_no_artifact_found: ignore
+ allow_forks: true
+
+ - name: Parse pull request number
+ id: pr-number
+ run: |
+ if [[ -f pr-number ]]
+ then
+ echo "pr-number=$(> "$GITHUB_OUTPUT"
+ fi
+
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: Download typing conformance suite commit
+ with:
+ name: conformance-suite-commit
+ run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
+ if_no_artifact_found: ignore
+ allow_forks: true
+
+ - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
+ name: "Download typing_conformance results"
+ id: download-typing_conformance_diff
+ if: steps.pr-number.outputs.pr-number
+ with:
+ name: typing_conformance_diagnostics_diff
+ workflow: typing_conformance.yaml
+ pr: ${{ steps.pr-number.outputs.pr-number }}
+ path: pr/typing_conformance_diagnostics_diff
+ workflow_conclusion: completed
+ if_no_artifact_found: ignore
+ allow_forks: true
+
+ - name: Generate comment content
+ id: generate-comment
+ if: ${{ steps.download-typing_conformance_diff.outputs.found_artifact == 'true' }}
+ run: |
+ # Guard against malicious typing_conformance results that symlink to a secret
+ # file on this runner
+ if [[ -L pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff ]]
+ then
+ echo "Error: typing_conformance_diagnostics.diff cannot be a symlink"
+ exit 1
+ fi
+
+ # Note this identifier is used to find the comment to update on
+ # subsequent runs
+ echo '' >> comment.txt
+
+ if [[ -f conformance-suite-commit ]]
+ then
+ echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(> comment.txt
+ else
+ echo "conformance-suite-commit file not found"
+ echo "## Diagnostic diff on typing conformance tests" >> comment.txt
+ fi
+
+ if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
+ echo '' >> comment.txt
+ echo 'Changes were detected when running ty on typing conformance tests
' >> comment.txt
+ echo '' >> comment.txt
+ echo '```diff' >> comment.txt
+ cat pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff >> comment.txt
+ echo '```' >> comment.txt
+ echo ' ' >> comment.txt
+ else
+ echo 'No changes detected when running ty on typing conformance tests ✅' >> comment.txt
+ fi
+
+ echo 'comment<> "$GITHUB_OUTPUT"
+ cat comment.txt >> "$GITHUB_OUTPUT"
+ echo 'EOF' >> "$GITHUB_OUTPUT"
+
+ - name: Find existing comment
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
+ if: steps.generate-comment.outcome == 'success'
+ id: find-comment
+ with:
+ issue-number: ${{ steps.pr-number.outputs.pr-number }}
+ comment-author: "github-actions[bot]"
+ body-includes: ""
+
+ - name: Create or update comment
+ if: steps.find-comment.outcome == 'success'
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
+ with:
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ issue-number: ${{ steps.pr-number.outputs.pr-number }}
+ body-path: comment.txt
+ edit-mode: replace
diff --git a/.github/zizmor.yml b/.github/zizmor.yml
index 383dcea02f..8eae6bd3f3 100644
--- a/.github/zizmor.yml
+++ b/.github/zizmor.yml
@@ -10,6 +10,8 @@ rules:
ignore:
- build-docker.yml
- publish-playground.yml
+ - ty-ecosystem-analyzer.yaml
+ - ty-ecosystem-report.yaml
excessive-permissions:
# it's hard to test what the impact of removing these ignores would be
# without actually running the release workflow...
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8649ac613e..b80bf6a375 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@ exclude: |
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty_python_semantic/resources/corpus/.*|
- crates/ty/docs/(configuration|rules|cli).md|
+ crates/ty/docs/(configuration|rules|cli|environment).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
@@ -16,7 +16,8 @@ exclude: |
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/tests/snapshots/.*|
crates/ruff_python_resolver/resources/.*|
- crates/ruff_python_resolver/tests/snapshots/.*
+ crates/ruff_python_resolver/tests/snapshots/.*|
+ crates/ty_completion_eval/truth/.*
)$
repos:
@@ -67,7 +68,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
- rev: v1.33.1
+ rev: v1.34.0
hooks:
- id: typos
@@ -81,17 +82,17 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.13
+ rev: v0.12.7
hooks:
- id: ruff-format
- - id: ruff
+ - id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
- rev: v3.5.3
+ rev: v3.6.2
hooks:
- id: prettier
types: [yaml]
@@ -99,12 +100,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.9.0
+ rev: v1.11.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.33.0
+ rev: 0.33.2
hooks:
- id: check-github-workflows
@@ -128,5 +129,10 @@ repos:
# but the integration only works if shellcheck is installed
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0"
+ - repo: https://github.com/shellcheck-py/shellcheck-py
+ rev: v0.10.0.1
+ hooks:
+ - id: shellcheck
+
ci:
skip: [cargo-fmt, dev-generate-all]
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 91c33bc2c6..ed69b26b29 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,4 +3,7 @@
"--all-features"
],
"rust-analyzer.check.command": "clippy",
-}
\ No newline at end of file
+ "search.exclude": {
+ "**/*.snap": true
+ }
+}
diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
index 7dc0583db5..491eb59cca 100644
--- a/BREAKING_CHANGES.md
+++ b/BREAKING_CHANGES.md
@@ -1,5 +1,60 @@
# Breaking Changes
+## 0.14.0
+
+- **Default to Python 3.10**
+
+ Ruff now defaults to Python 3.10 instead of 3.9 if no explicit Python
+ version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version)
+ or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires)
+ ([#20725](https://github.com/astral-sh/ruff/pull/20725))
+
+- **Default to Python 3.14 for syntax errors**
+
+ Ruff will default to the _latest_ supported Python version (3.14) when
+ checking for syntax errors without a Python version configured. The default
+ in all other cases, like applying lint rules, remains at the minimum
+ supported Python version (3.10).
+
+## 0.13.0
+
+- **Several rules can now add `from __future__ import annotations` automatically**
+
+ `TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
+ `lint.future-annotations` setting is enabled. This allows the rules to move
+ more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
+ use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
+ unquote more annotations (`UP037`).
+
+- **Full module paths are now used to verify first-party modules**
+
+ Ruff now checks that the full path to a module exists on disk before
+ categorizing it as a first-party import. This change makes first-party
+ import detection more accurate, helping to avoid false positives on local
+ directories with the same name as a third-party dependency, for example. See
+ the [FAQ
+ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
+
+- **Deprecated rules must now be selected by exact rule code**
+
+ Ruff will no longer activate deprecated rules selected by their group name
+ or prefix. As noted below, the two remaining deprecated rules were also
+ removed in this release, so this won't affect any current rules, but it will
+ still affect any deprecations in the future.
+
+- **The deprecated macOS configuration directory fallback has been removed**
+
+ Ruff will no longer look for a user-level configuration file at
+ `~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
+ deprecated in v0.5 in favor of using the [XDG
+ specification](https://specifications.freedesktop.org/basedir-spec/latest/)
+ (usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
+ fallback and accompanying deprecation warning have now been removed.
+
+- **[`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`) has been removed**
+
+- **[`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`) has been removed**
+
## 0.12.0
- **Detection of more syntax errors**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e35a64b5f4..96954d38c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,220 +1,50 @@
# Changelog
-## 0.12.1
+## 0.14.0
-### Preview features
-
-- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
-- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
-- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
-- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
-- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
-- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
-
-### Bug fixes
-
-- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
-- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
-- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
-- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
-- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
-- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
-- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
-- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
-- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
-- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
-- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
-- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
-- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
-- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
-- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
-- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
-- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
-- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
-- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
-- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
-- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
-- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
-
-### Rule changes
-
-- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
-- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
-- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
-- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
-- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
-- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
-- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
-- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
-- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
-- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
-- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
-- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
-- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
-- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
-- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
-- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
-
-### Server
-
-- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
-
-### Documentation
-
-- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
-- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
-- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
-- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
-- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
-- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
-- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
-- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
-
-### Other changes
-
-- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
-- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
-- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
-
-## 0.12.0
-
-Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
-guide and overview of the changes!
+Released on 2025-10-07.
### Breaking changes
-- **Detection of more syntax errors**
-
- Ruff now detects version-related syntax errors, such as the use of the `match`
- statement on Python versions before 3.10, and syntax errors emitted by
- CPython's compiler, such as irrefutable `match` patterns before the final
- `case` arm.
-
-- **New default Python version handling for syntax errors**
-
- Ruff will default to the *latest* supported Python version (3.13) when
- checking for the version-related syntax errors mentioned above to prevent
- false positives in projects without a Python version configured. The default
- in all other cases, like applying lint rules, is unchanged and remains at the
- minimum supported Python version (3.9).
-
-- **Updated f-string formatting**
-
- Ruff now formats multi-line f-strings with format specifiers to avoid adding a
- line break after the format specifier. This addresses a change to the Python
- grammar in version 3.13.4 that made such a line break a syntax error.
-
-- **`rust-toolchain.toml` is no longer included in source distributions**
-
- The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
- minimum supported Rust version (MSRV) for development and building release
- artifacts. However, when present in source distributions, it would also cause
- downstream package maintainers to pull in the same Rust toolchain, even if
- their available toolchain was MSRV-compatible.
-
-### Removed Rules
-
-The following rules have been removed:
-
-- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
- (`S320`)
-
-### Deprecated Rules
-
-The following rules have been deprecated:
-
-- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
-
-### Stabilization
-
-The following rules have been stabilized and are no longer in preview:
-
-- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
-- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
-- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
-- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
-- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
-- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
-- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
-- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
-- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
-- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
-- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
-- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
-- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
-- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
-- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
-- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
-- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
-- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
-- [`non-pep604-annotation-optional`] (`UP045`)
-- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
-- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
-- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
-
-The following behaviors have been stabilized:
-
-- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
- addition to list literals and variables.
-- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
-- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
- expressions to use `or` instead of an `if` expression, where possible.
-- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
- as inline comments.
-- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
- as well as lists and tuples of literal strings, as trusted input.
-- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
- include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
- plain `bool` annotations.
-- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
- `UP007` now applies only to `typing.Union`, while
- [`non-pep604-annotation-optional`] (`UP045`) checks for use of
- `typing.Optional`. `UP045` has also been stabilized in this release, but you
- may need to update existing `include`, `ignore`, or `noqa` settings to
- accommodate this change.
+- Update default and latest Python versions for 3.14 ([#20725](https://github.com/astral-sh/ruff/pull/20725))
### Preview features
-- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
-- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
+- \[`flake8-bugbear`\] Include certain guaranteed-mutable expressions: tuples, generators, and assignment expressions (`B006`) ([#20024](https://github.com/astral-sh/ruff/pull/20024))
+- \[`refurb`\] Add fixes for `FURB101` and `FURB103` ([#20520](https://github.com/astral-sh/ruff/pull/20520))
+- \[`ruff`\] Extend `FA102` with listed PEP 585-compatible APIs ([#20659](https://github.com/astral-sh/ruff/pull/20659))
### Bug fixes
-- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
-- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
-- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
-- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
-- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
-- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
-- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
-- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
-- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
-- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
-- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
-- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
-- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
-- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
-- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
-
-### Rule changes
-
-- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
-- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
-- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
-- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
-- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
-- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
-
-### Server
-
-- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
+- \[`flake8-annotations`\] Fix return type annotations to handle shadowed builtin symbols (`ANN201`, `ANN202`, `ANN204`, `ANN205`, `ANN206`) ([#20612](https://github.com/astral-sh/ruff/pull/20612))
+- \[`flynt`\] Fix f-string quoting for mixed quote joiners (`FLY002`) ([#20662](https://github.com/astral-sh/ruff/pull/20662))
+- \[`isort`\] Fix inserting required imports before future imports (`I002`) ([#20676](https://github.com/astral-sh/ruff/pull/20676))
+- \[`ruff`\] Handle argfile expansion errors gracefully ([#20691](https://github.com/astral-sh/ruff/pull/20691))
+- \[`ruff`\] Skip `RUF051` if `else`/`elif` block is present ([#20705](https://github.com/astral-sh/ruff/pull/20705))
+- \[`ruff`\] Improve handling of intermixed comments inside from-imports ([#20561](https://github.com/astral-sh/ruff/pull/20561))
### Documentation
-- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
-- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
-- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
-- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
+- \[`flake8-comprehensions`\] Clarify fix safety documentation (`C413`) ([#20640](https://github.com/astral-sh/ruff/pull/20640))
+
+### Contributors
+
+- [@danparizher](https://github.com/danparizher)
+- [@terror](https://github.com/terror)
+- [@TaKO8Ki](https://github.com/TaKO8Ki)
+- [@ntBre](https://github.com/ntBre)
+- [@njhearp](https://github.com/njhearp)
+- [@amyreese](https://github.com/amyreese)
+- [@IDrokin117](https://github.com/IDrokin117)
+- [@chirizxc](https://github.com/chirizxc)
+
+## 0.13.x
+
+See [changelogs/0.13.x](./changelogs/0.13.x.md)
+
+## 0.12.x
+
+See [changelogs/0.12.x](./changelogs/0.12.x.md)
## 0.11.x
@@ -259,12 +89,3 @@ See [changelogs/0.2.x](./changelogs/0.2.x.md)
## 0.1.x
See [changelogs/0.1.x](./changelogs/0.1.x.md)
-
-[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
-[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
-[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
-[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
-[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
-[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
-[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
-[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ca0605ab90..6c9a99aed0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,22 +7,38 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
> This guide is for Ruff. If you're looking to contribute to ty, please see [the ty contributing
> guide](https://github.com/astral-sh/ruff/blob/main/crates/ty/CONTRIBUTING.md).
+## Finding ways to help
+
+We label issues that would be good for a first time contributor as
+[`good first issue`](https://github.com/astral-sh/ruff/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
+These usually do not require significant experience with Rust or the Ruff code base.
+
+We label issues that we think are a good opportunity for subsequent contributions as
+[`help wanted`](https://github.com/astral-sh/ruff/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22).
+These require varying levels of experience with Rust and Ruff. Often, we want to accomplish these
+tasks but do not have the resources to do so ourselves.
+
+You don't need our permission to start on an issue we have labeled as appropriate for community
+contribution as described above. However, it's a good idea to indicate that you are going to work on
+an issue to avoid concurrent attempts to solve the same problem.
+
+Please check in with us before starting work on an issue that has not been labeled as appropriate
+for community contribution. We're happy to receive contributions for other issues, but it's
+important to make sure we have consensus on the solution to the problem first.
+
+Outside of issues with the labels above, issues labeled as
+[`bug`](https://github.com/astral-sh/ruff/issues?q=is%3Aopen+is%3Aissue+label%3A%22bug%22) are the
+best candidates for contribution. In contrast, issues labeled with `needs-decision` or
+`needs-design` are _not_ good candidates for contribution. Please do not open pull requests for
+issues with these labels.
+
+Please do not open pull requests for new features without prior discussion. While we appreciate
+exploration of new features, we will often close these pull requests immediately. Adding a
+new feature to ruff creates a long-term maintenance burden and requires strong consensus from the ruff
+team before it is appropriate to begin work on an implementation.
+
## The Basics
-Ruff welcomes contributions in the form of pull requests.
-
-For small changes (e.g., bug fixes), feel free to submit a PR.
-
-For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
-creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
-You can also join us on [Discord](https://discord.com/invite/astral-sh) to discuss your idea with the
-community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
-in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
-and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
-that are ready for contributions.
-
-If you have suggestions on how we might improve the contributing documentation, [let us know](https://github.com/astral-sh/ruff/discussions/5693)!
-
### Prerequisites
Ruff is written in Rust. You'll need to install the
@@ -266,6 +282,13 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
## 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:
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
@@ -298,10 +321,16 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
-### Creating a new release
+### Installing tools
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
+1. Install `npm`: `brew install npm` or similar
+
+### Creating a new release
+
+Commit each step of this process separately for easier review.
+
1. Run `./scripts/release.sh`; this command will:
- Generate a temporary virtual environment with `rooster`
@@ -314,6 +343,7 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
+ - Square brackets (eg, `[ruff]` project name) will be automatically escaped by `pre-commit`
Additionally, for minor releases:
@@ -353,13 +383,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Verify the GitHub release:
- 1. The Changelog should match the content of `CHANGELOG.md`
- 1. Append the contributors from the `scripts/release.sh` script
+ 1. The changelog should match the content of `CHANGELOG.md`
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
+ 1. Run `uv run --only-dev --no-sync scripts/update_schemastore.py --proto `
1. Once run successfully, you should follow the link in the output to create a PR.
1. If needed, update the [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) and
diff --git a/Cargo.lock b/Cargo.lock
index 2d7ea1cbd7..d823971f5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
@@ -23,12 +23,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-[[package]]
-name = "android-tzdata"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
-
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -56,9 +50,9 @@ dependencies = [
[[package]]
name = "anstream"
-version = "0.6.19"
+version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -77,60 +71,69 @@ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-lossy"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "934ff8719effd2023a48cf63e69536c1c3ced9d3895068f6f5cc9a4ff845e59b"
+checksum = "04d3a5dc826f84d0ea11882bb8054ff7f3d482602e11bb181101303a279ea01f"
dependencies = [
"anstyle",
]
[[package]]
name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
name = "anstyle-svg"
-version = "0.1.7"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3607949e9f6de49ea4bafe12f5e4fd73613ebf24795e48587302a8cc0e4bb35"
+checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde"
dependencies = [
- "anstream",
"anstyle",
"anstyle-lossy",
+ "anstyle-parse",
"html-escape",
"unicode-width 0.2.1",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
- "once_cell",
- "windows-sys 0.59.0",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
]
[[package]]
name = "anyhow"
-version = "1.0.98"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
[[package]]
name = "arc-swap"
@@ -201,9 +204,9 @@ dependencies = [
[[package]]
name = "autocfg"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
@@ -231,6 +234,26 @@ dependencies = [
"virtue",
]
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags 2.9.4",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -239,9 +262,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.9.1"
+version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
[[package]]
name = "block-buffer"
@@ -254,9 +289,9 @@ dependencies = [
[[package]]
name = "boxcar"
-version = "0.2.13"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa"
+checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
[[package]]
name = "bstr"
@@ -265,15 +300,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
- "regex-automata 0.4.9",
+ "regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
-version = "3.17.0"
+version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byteorder"
@@ -292,11 +327,11 @@ dependencies = [
[[package]]
name = "camino"
-version = "1.1.10"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
+checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603"
dependencies = [
- "serde",
+ "serde_core",
]
[[package]]
@@ -307,29 +342,39 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "castaway"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
-version = "1.2.23"
+version = "1.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
+checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
dependencies = [
+ "find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
-name = "cfg-if"
-version = "1.0.0"
+name = "cexpr"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
@@ -339,14 +384,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
-version = "0.4.41"
+version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
- "android-tzdata",
"iana-time-zone",
"num-traits",
- "windows-link",
+ "windows-link 0.2.0",
]
[[package]]
@@ -377,10 +421,21 @@ dependencies = [
]
[[package]]
-name = "clap"
-version = "4.5.40"
+name = "clang-sys"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [
"clap_builder",
"clap_derive",
@@ -388,9 +443,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.40"
+version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [
"anstream",
"anstyle",
@@ -401,9 +456,9 @@ dependencies = [
[[package]]
name = "clap_complete"
-version = "4.5.50"
+version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1"
+checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
dependencies = [
"clap",
]
@@ -421,9 +476,9 @@ dependencies = [
[[package]]
name = "clap_complete_nushell"
-version = "4.5.5"
+version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6a8b1593457dfc2fe539002b795710d022dc62a65bf15023f039f9760c7b18a"
+checksum = "0a0c951694691e65bf9d421d597d68416c22de9632e884c28412cb8cd8b73dce"
dependencies = [
"clap",
"clap_complete",
@@ -431,9 +486,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.5.40"
+version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
@@ -443,59 +498,67 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.7.4"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "clearscreen"
-version = "4.0.1"
+version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
+checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae"
dependencies = [
"nix 0.29.0",
"terminfo",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"which",
"windows-sys 0.59.0",
]
[[package]]
name = "codspeed"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93f4cce9c27c49c4f101fffeebb1826f41a9df2e7498b7cd4d95c0658b796c6c"
+checksum = "d0f62ea8934802f8b374bf691eea524c3aa444d7014f604dd4182a3667b69510"
dependencies = [
+ "anyhow",
+ "bindgen",
+ "cc",
"colored 2.2.0",
+ "glob",
"libc",
+ "nix 0.30.1",
"serde",
"serde_json",
+ "statrs",
"uuid",
]
[[package]]
name = "codspeed-criterion-compat"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c23d880a28a2aab52d38ca8481dd7a3187157d0a952196b6db1db3c8499725"
+checksum = "d87efbc015fc0ff1b2001cd87df01c442824de677e01a77230bf091534687abb"
dependencies = [
+ "clap",
"codspeed",
"codspeed-criterion-compat-walltime",
"colored 2.2.0",
+ "regex",
]
[[package]]
name = "codspeed-criterion-compat-walltime"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0a2f7365e347f4f22a67e9ea689bf7bc89900a354e22e26cf8a531a42c8fbb"
+checksum = "ae5713ace440123bb4f1f78dd068d46872cb8548bfe61f752e7b2ad2c06d7f00"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"codspeed",
- "criterion-plot",
+ "criterion-plot 0.5.0",
"is-terminal",
"itertools 0.10.5",
"num-traits",
@@ -511,20 +574,22 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8620a09dfaf37b3c45f982c4b65bd8f9b0203944da3ffa705c0fcae6b84655ff"
+checksum = "95b4214b974f8f5206497153e89db90274e623f06b00bf4b9143eeb7735d975d"
dependencies = [
+ "clap",
"codspeed",
"codspeed-divan-compat-macros",
"codspeed-divan-compat-walltime",
+ "regex",
]
[[package]]
name = "codspeed-divan-compat-macros"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30fe872bc4214626b35d3a1706a905d0243503bb6ba3bb7be2fc59083d5d680c"
+checksum = "a53f34a16cb70ce4fd9ad57e1db016f0718e434f34179ca652006443b9a39967"
dependencies = [
"divan-macros",
"itertools 0.14.0",
@@ -536,9 +601,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-walltime"
-version = "2.10.1"
+version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "104caa97b36d4092d89e24e4b103b40ede1edab03c0372d19e14a33f9393132b"
+checksum = "e8a5099050c8948dce488b8eaa2e68dc5cf571cb8f9fce99aaaecbdddb940bcd"
dependencies = [
"cfg-if",
"clap",
@@ -551,15 +616,15 @@ dependencies = [
[[package]]
name = "collection_literals"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271"
+checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8"
[[package]]
name = "colorchoice"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "colored"
@@ -610,10 +675,22 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
- "unicode-width 0.2.1",
"windows-sys 0.59.0",
]
+[[package]]
+name = "console"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width 0.2.1",
+ "windows-sys 0.61.0",
+]
+
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -645,11 +722,6 @@ name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
-dependencies = [
- "dashmap 5.5.3",
- "once_cell",
- "rustc-hash 1.1.0",
-]
[[package]]
name = "cpufeatures"
@@ -662,24 +734,24 @@ dependencies = [
[[package]]
name = "crc32fast"
-version = "1.4.2"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
+checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
- "criterion-plot",
+ "criterion-plot 0.6.0",
"itertools 0.13.0",
"num-traits",
"oorandom",
@@ -700,6 +772,16 @@ dependencies = [
"itertools 0.10.5",
]
+[[package]]
+name = "criterion-plot"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
+dependencies = [
+ "cast",
+ "itertools 0.13.0",
+]
+
[[package]]
name = "crossbeam"
version = "0.8.4"
@@ -758,9 +840,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
@@ -773,20 +855,42 @@ dependencies = [
]
[[package]]
-name = "ctrlc"
-version = "3.4.7"
+name = "csv"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
+checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3"
+dependencies = [
+ "dispatch",
"nix 0.30.1",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
[[package]]
name = "darling"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
@@ -794,9 +898,9 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
@@ -808,28 +912,15 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn",
]
-[[package]]
-name = "dashmap"
-version = "5.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
-dependencies = [
- "cfg-if",
- "hashbrown 0.14.5",
- "lock_api",
- "once_cell",
- "parking_lot_core",
-]
-
[[package]]
name = "dashmap"
version = "6.1.0"
@@ -846,9 +937,9 @@ dependencies = [
[[package]]
name = "derive-where"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b"
+checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
dependencies = [
"proc-macro2",
"quote",
@@ -916,9 +1007,15 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -961,9 +1058,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
-version = "1.0.19"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
@@ -991,12 +1088,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
-version = "0.3.12"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
[[package]]
@@ -1007,12 +1104,11 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
[[package]]
name = "escargot"
-version = "0.5.14"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
+checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf"
dependencies = [
"log",
- "once_cell",
"serde",
"serde_json",
]
@@ -1045,21 +1141,27 @@ dependencies = [
[[package]]
name = "filetime"
-version = "0.2.25"
+version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
-name = "flate2"
-version = "1.1.1"
+name = "find-msvc-tools"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
+checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1079,9 +1181,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@@ -1104,6 +1206,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1116,9 +1224,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
-version = "0.5.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea12180b12b82e9b7c01dfe91138208604961bb2bd7e93058d6786e5d770b104"
+checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
dependencies = [
"attribute-derive",
"quote",
@@ -1127,23 +1235,23 @@ dependencies = [
[[package]]
name = "get-size2"
-version = "0.5.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a37e4d438f7550dbd4938f1bcde41538653616513678d647665a7332ea3c030"
+checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
dependencies = [
"compact_str",
"get-size-derive2",
- "hashbrown 0.15.4",
+ "hashbrown 0.16.0",
"smallvec",
]
[[package]]
name = "getopts"
-version = "0.2.21"
+version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
- "unicode-width 0.1.14",
+ "unicode-width 0.2.1",
]
[[package]]
@@ -1154,7 +1262,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
@@ -1167,15 +1275,15 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
name = "glob"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
@@ -1186,8 +1294,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -1196,7 +1304,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"ignore",
"walkdir",
]
@@ -1219,22 +1327,31 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
-version = "0.15.4"
+version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+dependencies = [
+ "equivalent",
+]
+
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
- "hashbrown 0.15.4",
+ "hashbrown 0.15.5",
]
[[package]]
@@ -1245,15 +1362,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
-version = "0.3.9"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "hermit-abi"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "home"
@@ -1275,9 +1386,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.63"
+version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1346,9 +1457,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
@@ -1362,9 +1473,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
@@ -1391,9 +1502,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
-version = "1.0.3"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@@ -1420,7 +1531,7 @@ dependencies = [
"globset",
"log",
"memchr",
- "regex-automata 0.4.9",
+ "regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -1432,7 +1543,7 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
dependencies = [
- "hashbrown 0.15.4",
+ "hashbrown 0.15.5",
]
[[package]]
@@ -1447,25 +1558,26 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.9.0"
+version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
- "hashbrown 0.15.4",
+ "hashbrown 0.16.0",
"serde",
+ "serde_core",
]
[[package]]
name = "indicatif"
-version = "0.17.11"
+version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
+checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
- "console",
- "number_prefix",
+ "console 0.16.1",
"portable-atomic",
"unicode-width 0.2.1",
+ "unit-prefix",
"vt100",
"web-time",
]
@@ -1482,7 +1594,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"inotify-sys",
"libc",
]
@@ -1498,11 +1610,11 @@ dependencies = [
[[package]]
name = "insta"
-version = "1.43.1"
+version = "1.43.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
+checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
dependencies = [
- "console",
+ "console 0.15.11",
"globset",
"once_cell",
"pest",
@@ -1540,6 +1652,15 @@ dependencies = [
"memoffset",
]
+[[package]]
+name = "inventory"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -1567,7 +1688,7 @@ version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
- "hermit-abi 0.5.1",
+ "hermit-abi",
"libc",
"windows-sys 0.59.0",
]
@@ -1664,9 +1785,9 @@ dependencies = [
[[package]]
name = "jobserver"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -1680,9 +1801,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
-version = "0.3.77"
+version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1716,15 +1837,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.174"
+version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libcst"
-version = "1.8.2"
+version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae28ddc5b90c3e3146a21d051ca095cbc8d932ad8714cf65ddf71a9abb35684c"
+checksum = "052ef5d9fc958a51aeebdf3713573b36c6fd6eed0bf0e60e204d2c0f8cf19b9f"
dependencies = [
"annotate-snippets",
"libcst_derive",
@@ -1732,24 +1853,34 @@ dependencies = [
"paste",
"peg",
"regex",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
]
[[package]]
name = "libcst_derive"
-version = "1.8.2"
+version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc2de5c2f62bcf8a4f7290b1854388b262c4b68f1db1a3ee3ef6d4c1319b00a3"
+checksum = "a91a751afee92cbdd59d4bc6754c7672712eec2d30a308f23de4e3287b2929cb"
dependencies = [
"quote",
"syn",
]
[[package]]
-name = "libmimalloc-sys"
-version = "0.1.43"
+name = "libloading"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
dependencies = [
"cc",
"libc",
@@ -1757,11 +1888,11 @@ dependencies = [
[[package]]
name = "libredox"
-version = "0.1.3"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"libc",
"redox_syscall",
]
@@ -1780,9 +1911,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
-version = "0.9.4"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -1802,15 +1933,15 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.27"
+version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lsp-server"
-version = "0.7.8"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9"
+checksum = "7d6ada348dbc2703cbe7637b2dda05cff84d3da2819c24abcb305dd613e0ba2e"
dependencies = [
"crossbeam-channel",
"log",
@@ -1865,11 +1996,11 @@ dependencies = [
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
@@ -1901,9 +2032,9 @@ dependencies = [
[[package]]
name = "mimalloc"
-version = "0.1.47"
+version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
+checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
dependencies = [
"libmimalloc-sys",
]
@@ -1926,23 +2057,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1953,9 +2084,9 @@ checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
[[package]]
name = "newtype-uuid"
-version = "1.2.1"
+version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee3224f0e8be7c2a1ebc77ef9c3eecb90f55c6594399ee825de964526b3c9056"
+checksum = "a17d82edb1c8a6c20c238747ae7aae9181133e766bc92cd2556fdd764407d0d1"
dependencies = [
"uuid",
]
@@ -1966,7 +2097,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1978,7 +2109,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2002,12 +2133,11 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
-version = "8.0.0"
+version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
+checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
- "bitflags 2.9.1",
- "filetime",
+ "bitflags 2.9.4",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2016,7 +2146,7 @@ dependencies = [
"mio",
"notify-types",
"walkdir",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -2027,12 +2157,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -2046,26 +2175,26 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "hermit-abi 0.3.9",
+ "hermit-abi",
"libc",
]
-[[package]]
-name = "number_prefix"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
-
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
[[package]]
name = "oorandom"
version = "11.1.5"
@@ -2080,19 +2209,20 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
-version = "0.5.7"
+version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d31b8b7a99f71bdff4235faf9ce9eada0ad3562c8fbeb7d607d9f41a6ec569d"
+checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
dependencies = [
"indexmap",
"serde",
+ "serde_core",
]
[[package]]
name = "os_pipe"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
+checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -2100,24 +2230,18 @@ dependencies = [
[[package]]
name = "os_str_bytes"
-version = "7.1.0"
+version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c86e2db86dd008b4c88c77a9bb83d9286bf77204e255bb3fda3b2eebcae66b62"
+checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c"
dependencies = [
"memchr",
]
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
[[package]]
name = "parking_lot"
-version = "0.12.3"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -2125,15 +2249,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.10"
+version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -2224,7 +2348,7 @@ dependencies = [
"once_cell",
"pep440_rs",
"regex",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"serde",
"smallvec",
"thiserror 1.0.69",
@@ -2236,26 +2360,26 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
-version = "2.8.0"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
+checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
dependencies = [
"memchr",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"ucd-trie",
]
[[package]]
name = "pest_derive"
-version = "2.8.0"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
+checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
dependencies = [
"pest",
"pest_generator",
@@ -2263,9 +2387,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.8.0"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
+checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
dependencies = [
"pest",
"pest_meta",
@@ -2276,11 +2400,10 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.8.0"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
+checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
dependencies = [
- "once_cell",
"pest",
"sha2",
]
@@ -2337,9 +2460,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
@@ -2352,9 +2475,9 @@ dependencies = [
[[package]]
name = "potential_utf"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@@ -2406,10 +2529,20 @@ dependencies = [
]
[[package]]
-name = "proc-macro-crate"
-version = "3.3.0"
+name = "prettyplease"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
@@ -2427,24 +2560,24 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyproject-toml"
-version = "0.13.5"
+version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663"
+checksum = "ec768e063102b426e8962989758115e8659485124de9207bc365fab524125d65"
dependencies = [
"indexmap",
"pep440_rs",
"pep508_rs",
"serde",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"toml",
]
@@ -2459,7 +2592,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"uuid",
]
@@ -2525,9 +2658,15 @@ dependencies = [
[[package]]
name = "r-efi"
-version = "5.2.0"
+version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
@@ -2542,9 +2681,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -2590,9 +2729,9 @@ dependencies = [
[[package]]
name = "rayon"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@@ -2600,9 +2739,9 @@ dependencies = [
[[package]]
name = "rayon-core"
-version = "1.12.1"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -2610,73 +2749,58 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.5.12"
+version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
]
[[package]]
name = "redox_users"
-version = "0.5.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
]
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
name = "regex-automata"
-version = "0.1.10"
+version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
[[package]]
name = "regex-lite"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
+checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]]
name = "regex-syntax"
-version = "0.6.29"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "ron"
@@ -2691,13 +2815,13 @@ dependencies = [
[[package]]
name = "ruff"
-version = "0.12.1"
+version = "0.14.0"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"cachedir",
"clap",
"clap_complete_command",
@@ -2730,18 +2854,19 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
+ "ruff_python_trivia",
"ruff_server",
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"serde",
"serde_json",
"shellexpand",
"strum",
"tempfile",
"test-case",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"tikv-jemallocator",
"toml",
"tracing",
@@ -2780,7 +2905,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"serde",
"serde_json",
"tikv-jemallocator",
@@ -2808,8 +2933,7 @@ dependencies = [
"anstyle",
"arc-swap",
"camino",
- "countme",
- "dashmap 6.1.0",
+ "dashmap",
"dunce",
"etcetera",
"filetime",
@@ -2819,22 +2943,29 @@ dependencies = [
"insta",
"matchit",
"path-slash",
+ "pathdiff",
+ "quick-junit",
"ruff_annotate_snippets",
"ruff_cache",
+ "ruff_diagnostics",
+ "ruff_memory_usage",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"schemars",
"serde",
+ "serde_json",
+ "similar",
"tempfile",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"tracing",
"tracing-subscriber",
+ "ty_static",
"web-time",
"zip",
]
@@ -2878,6 +3009,8 @@ dependencies = [
"tracing-subscriber",
"ty",
"ty_project",
+ "ty_python_semantic",
+ "ty_static",
"url",
]
@@ -2885,6 +3018,7 @@ dependencies = [
name = "ruff_diagnostics"
version = "0.0.0"
dependencies = [
+ "get-size2",
"is-macro",
"ruff_text_size",
"serde",
@@ -2898,7 +3032,7 @@ dependencies = [
"ruff_cache",
"ruff_macros",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"schemars",
"serde",
"static_assertions",
@@ -2912,6 +3046,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
+ "memchr",
"ruff_cache",
"ruff_db",
"ruff_linter",
@@ -2937,17 +3072,17 @@ dependencies = [
[[package]]
name = "ruff_linter"
-version = "0.12.1"
+version = "0.14.0"
dependencies = [
"aho-corasick",
"anyhow",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"clap",
"colored 3.0.0",
"fern",
"glob",
"globset",
- "hashbrown 0.15.4",
+ "hashbrown 0.16.0",
"imperative",
"insta",
"is-macro",
@@ -2959,12 +3094,9 @@ dependencies = [
"memchr",
"natord",
"path-absolutize",
- "pathdiff",
"pep440_rs",
"pyproject-toml",
- "quick-junit",
"regex",
- "ruff_annotate_snippets",
"ruff_cache",
"ruff_db",
"ruff_diagnostics",
@@ -2972,6 +3104,7 @@ dependencies = [
"ruff_notebook",
"ruff_python_ast",
"ruff_python_codegen",
+ "ruff_python_importer",
"ruff_python_index",
"ruff_python_literal",
"ruff_python_parser",
@@ -2980,7 +3113,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"schemars",
"serde",
"serde_json",
@@ -2990,7 +3123,7 @@ dependencies = [
"strum_macros",
"tempfile",
"test-case",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"toml",
"typed-arena",
"unicode-normalization",
@@ -3011,13 +3144,21 @@ dependencies = [
"syn",
]
+[[package]]
+name = "ruff_memory_usage"
+version = "0.0.0"
+dependencies = [
+ "get-size2",
+ "ordermap",
+]
+
[[package]]
name = "ruff_notebook"
version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
- "rand 0.9.1",
+ "rand 0.9.2",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -3025,7 +3166,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"uuid",
]
@@ -3041,7 +3182,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"compact_str",
"get-size2",
"is-macro",
@@ -3052,11 +3193,11 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"schemars",
"serde",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
]
[[package]]
@@ -3102,7 +3243,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"schemars",
"serde",
@@ -3110,10 +3251,25 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"tracing",
]
+[[package]]
+name = "ruff_python_importer"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "insta",
+ "ruff_diagnostics",
+ "ruff_python_ast",
+ "ruff_python_codegen",
+ "ruff_python_parser",
+ "ruff_python_trivia",
+ "ruff_source_file",
+ "ruff_text_size",
+]
+
[[package]]
name = "ruff_python_index"
version = "0.0.0"
@@ -3129,7 +3285,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3140,7 +3296,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"bstr",
"compact_str",
"get-size2",
@@ -3151,7 +3307,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"serde",
"serde_json",
"static_assertions",
@@ -3165,7 +3321,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"insta",
"is-macro",
"ruff_cache",
@@ -3175,7 +3331,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"schemars",
"serde",
"smallvec",
@@ -3186,7 +3342,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"unicode-ident",
]
@@ -3223,6 +3379,7 @@ dependencies = [
"lsp-server",
"lsp-types",
"regex",
+ "ruff_db",
"ruff_diagnostics",
"ruff_formatter",
"ruff_linter",
@@ -3235,11 +3392,11 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"serde",
"serde_json",
"shellexpand",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"toml",
"tracing",
"tracing-log",
@@ -3269,7 +3426,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
-version = "0.12.1"
+version = "0.14.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3324,13 +3481,14 @@ dependencies = [
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"schemars",
"serde",
"shellexpand",
"strum",
"tempfile",
"toml",
+ "unicode-normalization",
]
[[package]]
@@ -3343,12 +3501,6 @@ dependencies = [
"serde_derive",
]
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -3363,22 +3515,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
-version = "1.0.7"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
[[package]]
name = "rustversion"
-version = "1.0.20"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -3388,21 +3540,21 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
-version = "0.22.0"
-source = "git+https://github.com/salsa-rs/salsa?rev=0666e2018bc35376b1ac4f98906f2d04d11e5fe4#0666e2018bc35376b1ac4f98906f2d04d11e5fe4"
+version = "0.23.0"
+source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"boxcar",
"compact_str",
"crossbeam-queue",
"crossbeam-utils",
- "hashbrown 0.15.4",
+ "hashbrown 0.15.5",
"hashlink",
"indexmap",
"intrusive-collections",
+ "inventory",
"parking_lot",
"portable-atomic",
- "rayon",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa-macro-rules",
"salsa-macros",
"smallvec",
@@ -3412,13 +3564,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
-version = "0.22.0"
-source = "git+https://github.com/salsa-rs/salsa?rev=0666e2018bc35376b1ac4f98906f2d04d11e5fe4#0666e2018bc35376b1ac4f98906f2d04d11e5fe4"
+version = "0.23.0"
+source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
[[package]]
name = "salsa-macros"
-version = "0.22.0"
-source = "git+https://github.com/salsa-rs/salsa?rev=0666e2018bc35376b1ac4f98906f2d04d11e5fe4#0666e2018bc35376b1ac4f98906f2d04d11e5fe4"
+version = "0.23.0"
+source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"proc-macro2",
"quote",
@@ -3473,10 +3625,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
+ "serde_core",
"serde_derive",
]
@@ -3492,10 +3645,19 @@ dependencies = [
]
[[package]]
-name = "serde_derive"
-version = "1.0.219"
+name = "serde_core"
+version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.226"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
@@ -3515,14 +3677,15 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
@@ -3538,11 +3701,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "0.6.9"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
- "serde",
+ "serde_core",
]
[[package]]
@@ -3556,9 +3719,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.12.0"
+version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
+checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
dependencies = [
"serde",
"serde_derive",
@@ -3567,9 +3730,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.12.0"
+version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
+checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
dependencies = [
"darling",
"proc-macro2",
@@ -3671,6 +3834,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+[[package]]
+name = "statrs"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
+dependencies = [
+ "approx",
+ "num-traits",
+]
+
[[package]]
name = "strip-ansi-escapes"
version = "0.2.1"
@@ -3688,31 +3861,30 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
-version = "0.27.1"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
-version = "0.27.1"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "rustversion",
"syn",
]
[[package]]
name = "syn"
-version = "2.0.104"
+version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -3731,16 +3903,22 @@ dependencies = [
]
[[package]]
-name = "tempfile"
-version = "3.20.0"
+name = "tap"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
[[package]]
@@ -3754,12 +3932,12 @@ dependencies = [
[[package]]
name = "terminal_size"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
+checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -3830,11 +4008,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.12"
+version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
- "thiserror-impl 2.0.12",
+ "thiserror-impl 2.0.16",
]
[[package]]
@@ -3850,9 +4028,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.12"
+version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@@ -3861,12 +4039,11 @@ dependencies = [
[[package]]
name = "thread_local"
-version = "1.1.8"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
- "once_cell",
]
[[package]]
@@ -3920,9 +4097,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.9.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -3935,44 +4112,54 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
-version = "0.8.23"
+version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
-[[package]]
-name = "toml_datetime"
-version = "0.6.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.22.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
- "serde",
+ "serde_core",
"serde_spanned",
"toml_datetime",
- "toml_write",
+ "toml_parser",
+ "toml_writer",
"winnow",
]
[[package]]
-name = "toml_write"
-version = "0.1.2"
+name = "toml_datetime"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "tracing"
@@ -3988,9 +4175,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
@@ -3999,9 +4186,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
@@ -4020,9 +4207,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
-version = "0.3.9"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1"
+checksum = "04d4e11e0e27acef25a47f27e9435355fecdc488867fa2bc90e75b0700d2823d"
dependencies = [
"indicatif",
"tracing",
@@ -4043,15 +4230,15 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -4079,8 +4266,8 @@ dependencies = [
"argfile",
"clap",
"clap_complete_command",
+ "clearscreen",
"colored 3.0.0",
- "countme",
"crossbeam",
"ctrlc",
"dunce",
@@ -4100,27 +4287,72 @@ dependencies = [
"tracing",
"tracing-flame",
"tracing-subscriber",
+ "ty_combine",
"ty_project",
"ty_python_semantic",
"ty_server",
+ "ty_static",
"wild",
]
+[[package]]
+name = "ty_combine"
+version = "0.0.0"
+dependencies = [
+ "ordermap",
+ "ruff_db",
+ "ruff_python_ast",
+ "ty_python_semantic",
+]
+
+[[package]]
+name = "ty_completion_eval"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bstr",
+ "clap",
+ "csv",
+ "regex",
+ "ruff_db",
+ "ruff_text_size",
+ "serde",
+ "tempfile",
+ "toml",
+ "ty_ide",
+ "ty_project",
+ "ty_python_semantic",
+ "walkdir",
+]
+
[[package]]
name = "ty_ide"
version = "0.0.0"
dependencies = [
+ "bitflags 2.9.4",
+ "camino",
+ "get-size2",
"insta",
+ "itertools 0.14.0",
+ "rayon",
+ "regex",
"ruff_db",
+ "ruff_diagnostics",
+ "ruff_index",
+ "ruff_memory_usage",
"ruff_python_ast",
+ "ruff_python_codegen",
+ "ruff_python_importer",
"ruff_python_parser",
+ "ruff_python_trivia",
+ "ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"smallvec",
"tracing",
+ "ty_project",
"ty_python_semantic",
- "ty_vendored",
]
[[package]]
@@ -4139,23 +4371,25 @@ dependencies = [
"pep440_rs",
"rayon",
"regex",
- "regex-automata 0.4.9",
+ "regex-automata",
"ruff_cache",
"ruff_db",
"ruff_macros",
+ "ruff_memory_usage",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"schemars",
"serde",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"toml",
"tracing",
- "ty_ide",
+ "ty_combine",
"ty_python_semantic",
+ "ty_static",
"ty_vendored",
]
@@ -4164,16 +4398,16 @@ name = "ty_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
+ "bitvec",
"camino",
"colored 3.0.0",
"compact_str",
- "countme",
"dir-test",
"drop_bomb",
"get-size2",
"glob",
- "hashbrown 0.15.4",
+ "hashbrown 0.16.0",
"indexmap",
"insta",
"itertools 0.14.0",
@@ -4185,6 +4419,7 @@ dependencies = [
"ruff_db",
"ruff_index",
"ruff_macros",
+ "ruff_memory_usage",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
@@ -4192,19 +4427,21 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"schemars",
"serde",
"smallvec",
"static_assertions",
+ "strsim",
"strum",
"strum_macros",
"tempfile",
"test-case",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"tracing",
"ty_python_semantic",
+ "ty_static",
"ty_test",
"ty_vendored",
]
@@ -4214,25 +4451,42 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
+ "bitflags 2.9.4",
"crossbeam",
+ "dunce",
+ "insta",
"jod-thread",
"libc",
"lsp-server",
"lsp-types",
+ "regex",
"ruff_db",
+ "ruff_macros",
"ruff_notebook",
+ "ruff_python_ast",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"salsa",
"serde",
"serde_json",
"shellexpand",
+ "tempfile",
+ "thiserror 2.0.16",
"tracing",
"tracing-subscriber",
+ "ty_combine",
"ty_ide",
"ty_project",
"ty_python_semantic",
+ "ty_vendored",
+]
+
+[[package]]
+name = "ty_static"
+version = "0.0.1"
+dependencies = [
+ "ruff_macros",
]
[[package]]
@@ -4240,11 +4494,12 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
- "bitflags 2.9.1",
+ "bitflags 2.9.4",
"camino",
"colored 3.0.0",
"insta",
"memchr",
+ "path-slash",
"regex",
"ruff_db",
"ruff_index",
@@ -4253,16 +4508,17 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"rustc-stable-hash",
"salsa",
"serde",
"smallvec",
"tempfile",
- "thiserror 2.0.12",
+ "thiserror 2.0.16",
"toml",
"tracing",
"ty_python_semantic",
+ "ty_static",
"ty_vendored",
]
@@ -4272,6 +4528,7 @@ version = "0.0.0"
dependencies = [
"path-slash",
"ruff_db",
+ "static_assertions",
"walkdir",
"zip",
]
@@ -4291,6 +4548,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"serde-wasm-bindgen",
+ "tracing",
"ty_ide",
"ty_project",
"ty_python_semantic",
@@ -4361,15 +4619,15 @@ dependencies = [
[[package]]
name = "unicode-id"
-version = "0.3.5"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
+checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
[[package]]
name = "unicode-ident"
-version = "1.0.18"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "unicode-normalization"
@@ -4414,6 +4672,12 @@ dependencies = [
"rand 0.8.5",
]
+[[package]]
+name = "unit-prefix"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
+
[[package]]
name = "unscanny"
version = "0.1.0"
@@ -4428,9 +4692,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
-version = "2.5.4"
+version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -4464,22 +4728,22 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.17.0"
+version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
- "rand 0.9.1",
+ "rand 0.9.2",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
-version = "1.17.0"
+version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7"
+checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f"
dependencies = [
"proc-macro2",
"quote",
@@ -4576,36 +4840,46 @@ dependencies = [
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
-version = "0.14.2+wasi-0.2.4"
+version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
- "wit-bindgen-rt",
+ "wasip2",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
+ "wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
dependencies = [
"bumpalo",
"log",
@@ -4617,9 +4891,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.50"
+version = "0.4.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
dependencies = [
"cfg-if",
"js-sys",
@@ -4630,9 +4904,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4640,9 +4914,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
dependencies = [
"proc-macro2",
"quote",
@@ -4653,18 +4927,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.50"
+version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
+checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c"
dependencies = [
"js-sys",
"minicov",
@@ -4675,9 +4949,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.50"
+version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
+checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d"
dependencies = [
"proc-macro2",
"quote",
@@ -4686,9 +4960,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.77"
+version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4706,11 +4980,10 @@ dependencies = [
[[package]]
name = "which"
-version = "7.0.3"
+version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
+checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
- "either",
"env_home",
"rustix",
"winsafe",
@@ -4725,46 +4998,24 @@ dependencies = [
"glob",
]
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
[[package]]
name = "winapi-util"
-version = "0.1.9"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.0",
]
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows-core"
-version = "0.61.1"
+version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40"
+checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link",
+ "windows-link 0.2.0",
"windows-result",
"windows-strings",
]
@@ -4793,26 +5044,32 @@ dependencies = [
[[package]]
name = "windows-link"
-version = "0.1.1"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
-version = "0.3.3"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d"
+checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
- "windows-link",
+ "windows-link 0.2.0",
]
[[package]]
name = "windows-strings"
-version = "0.4.1"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a"
+checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
- "windows-link",
+ "windows-link 0.2.0",
]
[[package]]
@@ -4821,7 +5078,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -4830,7 +5087,25 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
+dependencies = [
+ "windows-link 0.2.0",
]
[[package]]
@@ -4839,14 +5114,31 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
]
[[package]]
@@ -4855,42 +5147,84 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -4898,10 +5232,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
-name = "winnow"
-version = "0.7.10"
+name = "windows_x86_64_msvc"
+version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
@@ -4913,13 +5253,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
+name = "wit-bindgen"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
-dependencies = [
- "bitflags 2.9.1",
-]
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
@@ -4927,6 +5264,15 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
[[package]]
name = "yansi"
version = "1.0.1"
@@ -4959,18 +5305,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.25"
+version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.25"
+version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
@@ -5011,9 +5357,9 @@ dependencies = [
[[package]]
name = "zerovec"
-version = "0.11.2"
+version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",
@@ -5065,9 +5411,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
-version = "2.0.15+zstd.1.5.7"
+version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
diff --git a/Cargo.toml b/Cargo.toml
index 44e833c1e2..e7fea1cd0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
-rust-version = "1.86"
+rust-version = "1.88"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -23,11 +23,13 @@ ruff_graph = { path = "crates/ruff_graph" }
ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
+ruff_memory_usage = { path = "crates/ruff_memory_usage" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
+ruff_python_importer = { path = "crates/ruff_python_importer" }
ruff_python_index = { path = "crates/ruff_python_index" }
ruff_python_literal = { path = "crates/ruff_python_literal" }
ruff_python_parser = { path = "crates/ruff_python_parser" }
@@ -40,10 +42,13 @@ ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
+ty_combine = { path = "crates/ty_combine" }
+ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_server = { path = "crates/ty_server" }
+ty_static = { path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { path = "crates/ty_vendored" }
@@ -56,20 +61,24 @@ assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
+bitvec = { version = "1.0.1", default-features = false, features = [
+ "alloc",
+] }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
camino = { version = "1.1.7" }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
-divan = { package = "codspeed-divan-compat", version = "2.10.1" }
-codspeed-criterion-compat = { version = "2.6.0", default-features = false }
+csv = { version = "1.3.1" }
+divan = { package = "codspeed-divan-compat", version = "4.0.4" }
+codspeed-criterion-compat = { version = "4.0.4", default-features = false }
colored = { version = "3.0.0" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
-criterion = { version = "0.6.0", default-features = false }
+criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
@@ -79,16 +88,16 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
-get-size2 = { version = "0.5.0", features = [
+get-size2 = { version = "0.7.0", features = [
"derive",
"smallvec",
"hashbrown",
- "compact-str"
+ "compact-str",
] }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
-hashbrown = { version = "0.15.0", default-features = false, features = [
+hashbrown = { version = "0.16.0", default-features = false, features = [
"raw-entry",
"equivalent",
"inline-more",
@@ -98,7 +107,7 @@ ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indexmap = { version = "2.6.0" }
-indicatif = { version = "0.17.8" }
+indicatif = { version = "0.18.0" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
@@ -109,7 +118,7 @@ jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "1.0.0" }
libc = { version = "0.2.153" }
-libcst = { version = "1.1.0", default-features = false }
+libcst = { version = "1.8.4", default-features = false }
log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" }
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
@@ -137,7 +146,12 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
-salsa = { git = "https://github.com/salsa-rs/salsa", rev = "0666e2018bc35376b1ac4f98906f2d04d11e5fe4" }
+salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "29ab321b45d00daa4315fa2a06f7207759a8c87e", default-features = false, features = [
+ "compact_str",
+ "macros",
+ "salsa_unstable",
+ "inventory",
+] }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -149,7 +163,7 @@ serde_with = { version = "3.6.0", default-features = false, features = [
] }
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
-smallvec = { version = "1.13.2" }
+smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",
@@ -164,16 +178,16 @@ tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thiserror = { version = "2.0.0" }
tikv-jemallocator = { version = "0.6.0" }
-toml = { version = "0.8.11" }
+toml = { version = "0.9.0" }
tracing = { version = "0.1.40" }
tracing-flame = { version = "0.2.0" }
-tracing-indicatif = { version = "0.3.6" }
+tracing-indicatif = { version = "0.3.11" }
tracing-log = { version = "0.2.0" }
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"env-filter",
"fmt",
"ansi",
- "smallvec"
+ "smallvec",
] }
tryfn = { version = "0.2.1" }
typed-arena = { version = "2.0.2" }
@@ -183,11 +197,7 @@ unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
url = { version = "2.5.0" }
-uuid = { version = "1.6.1", features = [
- "v4",
- "fast-rng",
- "macro-diagnostics",
-] }
+uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.42" }
@@ -195,7 +205,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
-ignored = ["getrandom", "ruff_options_metadata", "uuid"]
+ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2", "ty_completion_eval"]
[workspace.lints.rust]
@@ -208,6 +218,8 @@ unexpected_cfgs = { level = "warn", check-cfg = [
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -2 }
+# Enabled at the crate level
+disallowed_methods = "allow"
# Allowed pedantic lints
char_lit_as_u8 = "allow"
collapsible_else_if = "allow"
@@ -222,8 +234,8 @@ must_use_candidate = "allow"
similar_names = "allow"
single_match_else = "allow"
too_many_lines = "allow"
-needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
-unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
+needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
+unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
needless_raw_string_hashes = "allow"
# Disallowed restriction lints
@@ -242,10 +254,19 @@ rest_pat_in_fully_bound_structs = "warn"
redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
+# This lint sometimes flags code whose `if` and `else`
+# bodies could be flipped when a `!` operator is removed.
+# While perhaps sometimes a good idea, it is also often
+# not a good idea due to other factors impacting
+# readability. For example, if flipping the bodies results
+# in the `if` being an order of magnitude bigger than the
+# `else`, then some might consider that harder to read.
+if_not_else = "allow"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
+
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
diff --git a/LICENSE b/LICENSE
index f5c3b02bec..655a0c76fc 100644
--- a/LICENSE
+++ b/LICENSE
@@ -25,437 +25,6 @@ end of terms and conditions
The externally maintained libraries from which parts of the Software is derived
are:
-- flake8-comprehensions, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2017 Adam Johnson
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-no-pep420, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2020 Adam Johnson
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-tidy-imports, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2017 Adam Johnson
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-return, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2019 Afonasev Evgeniy
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-2020, licensed as follows:
- """
- Copyright (c) 2019 Anthony Sottile
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- pyupgrade, licensed as follows:
- """
- Copyright (c) 2017 Anthony Sottile
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- flake8-blind-except, licensed as follows:
- """
- The MIT License (MIT)
-
- Copyright (c) 2014 Elijah Andrews
-
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- """
-
-- flake8-gettext, licensed as follows:
- """
- BSD Zero Clause License
-
- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
-
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- """
-
-- flake8-implicit-str-concat, licensed as follows:
- """
- The MIT License (MIT)
-
- Copyright (c) 2019 Dylan Turner
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- flake8-debugger, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2016 Joseph Kahn
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-pyi, licensed as follows:
- """
- The MIT License (MIT)
-
- Copyright (c) 2016 Łukasz Langa
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-print, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2016 Joseph Kahn
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-import-conventions, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2021 João Palmeiro
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-simplify, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2020 Martin Thoma
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-slots, licensed as follows:
- """
- Copyright (c) 2021 Dominic Davis-Foster
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
- OR OTHER DEALINGS IN THE SOFTWARE.
- """
-
-- flake8-todos, licensed as follows:
- """
- Copyright (c) 2019 EclecticIQ. All rights reserved.
- Copyright (c) 2020 Gram . All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
-
- 1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- 3. Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived from this
- software without specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- """
-
-- flake8-unused-arguments, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2019 Nathan Hoad
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- pygrep-hooks, licensed as follows:
- """
- Copyright (c) 2018 Anthony Sottile
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
- autoflake, licensed as follows:
"""
Copyright (C) 2012-2018 Steven Myint
@@ -530,7 +99,32 @@ are:
SOFTWARE.
"""
-- flake8-bugbear, licensed as follows:
+- flake8-eradicate, licensed as follows:
+ """
+ MIT License
+
+ Copyright (c) 2018 Nikita Sobolev
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- flake8-pyi, licensed as follows:
"""
The MIT License (MIT)
@@ -555,60 +149,11 @@ are:
SOFTWARE.
"""
-- flake8-commas, licensed as follows:
- """
- The MIT License (MIT)
-
- Copyright (c) 2017 Thomas Grainger.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
-
-
- Portions of this flake8-commas Software may utilize the following
- copyrighted material, the use of which is hereby acknowledged.
-
- Original flake8-commas: https://github.com/trevorcreech/flake8-commas/commit/e8563b71b1d5442e102c8734c11cb5202284293d
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- flynt, licensed as follows:
+- flake8-simplify, licensed as follows:
"""
MIT License
- Copyright (c) 2019-2022 Ilya Kamenshchikov
+ Copyright (c) 2020 Martin Thoma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -654,31 +199,27 @@ are:
THE SOFTWARE.
"""
-- pep8-naming, licensed as follows:
+- pygrep-hooks, licensed as follows:
"""
- Copyright © 2013 Florent Xicluna
+ Copyright (c) 2018 Anthony Sottile
- Licensed under the terms of the Expat License
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
- Permission is hereby granted, free of charge, to any person
- obtaining a copy of this software and associated documentation files
- (the "Software"), to deal in the Software without restriction,
- including without limitation the rights to use, copy, modify, merge,
- publish, distribute, sublicense, and/or sell copies of the Software,
- and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
- BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
"""
- pycodestyle, licensed as follows:
@@ -762,11 +303,60 @@ are:
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
-- flake8-use-pathlib, licensed as follows:
+- Pyright, licensed as follows:
"""
MIT License
- Copyright (c) 2021 Rodolphe Pelloux-Prayer
+ Pyright - A static type checker for the Python language
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+ """
+
+- pyupgrade, licensed as follows:
+ """
+ Copyright (c) 2017 Anthony Sottile
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ """
+
+- rome/tools, licensed under the MIT license:
+ """
+ MIT License
+
+ Copyright (c) Rome Tools, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -812,514 +402,6 @@ are:
SOFTWARE.
"""
-- flake8-annotations, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2019 - Present S. Co1
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-async, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2022 Cooper Lees
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-type-checking, licensed as follows:
- """
- Copyright (c) 2021, Sondre Lillebø Gundersen
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
- * Neither the name of pytest-{{ cookiecutter.plugin_name }} nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- """
-
-- flake8-bandit, licensed as follows:
- """
- Copyright (c) 2017 Tyler Wince
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- flake8-eradicate, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2018 Nikita Sobolev
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-quotes, licensed as follows:
- """
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
-
-- flake8-logging-format, licensed as follows:
- """
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- """
-
-- flake8-raise, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2020 Jon Dufresne
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-self, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2023 Korijn van Golen
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-django, licensed under the GPL license.
-
-- perflint, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2022 Anthony Shaw
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-logging, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2023 Adam Johnson
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- flake8-trio, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2022 Zac Hatfield-Dodds
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- Pyright, licensed as follows:
- """
- MIT License
-
- Pyright - A static type checker for the Python language
- Copyright (c) Microsoft Corporation. All rights reserved.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE
- """
-
- rust-analyzer/text-size, licensed under the MIT license:
"""
Permission is hereby granted, free of charge, to any
@@ -1346,53 +428,3 @@ are:
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
-
-- rome/tools, licensed under the MIT license:
- """
- MIT License
-
- Copyright (c) Rome Tools, Inc. and its affiliates.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
-
-- pydoclint, licensed as follows:
- """
- MIT License
-
- Copyright (c) 2023 jsh9
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- """
diff --git a/README.md b/README.md
index 5832fed435..bf5b1ce71a 100644
--- a/README.md
+++ b/README.md
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
-curl -LsSf https://astral.sh/ruff/0.12.1/install.sh | sh
-powershell -c "irm https://astral.sh/ruff/0.12.1/install.ps1 | iex"
+curl -LsSf https://astral.sh/ruff/0.14.0/install.sh | sh
+powershell -c "irm https://astral.sh/ruff/0.14.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.12.1
+ rev: v0.14.0
hooks:
# Run the linter.
- id: ruff-check
@@ -421,14 +421,16 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including:
-- [Albumentations](https://github.com/albumentations-team/albumentations)
+- [Albumentations](https://github.com/albumentations-team/AlbumentationsX)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
+- [Anki](https://apps.ankiweb.net/)
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow)
- AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core))
- [Babel](https://github.com/python-babel/babel)
- Benchling ([Refac](https://github.com/benchling/refac))
- [Bokeh](https://github.com/bokeh/bokeh)
+- Capital One ([datacompy](https://github.com/capitalone/datacompy))
- CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox))
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- CERN ([Indico](https://getindico.io/))
@@ -505,6 +507,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Streamlit](https://github.com/streamlit/streamlit)
- [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair)
+- [Weblate](https://weblate.org/)
- WordPress ([Openverse](https://github.com/WordPress/openverse))
- [ZenML](https://github.com/zenml-io/zenml)
- [Zulip](https://github.com/zulip/zulip)
diff --git a/_typos.toml b/_typos.toml
index 24406f10bb..c4ff9f6151 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -31,6 +31,7 @@ extend-ignore-re = [
"typ",
# TODO: Remove this once the `TYP` redirects are removed from `rule_redirects.rs`
"TYP",
+ "ntBre"
]
[default.extend-identifiers]
diff --git a/changelogs/0.12.x.md b/changelogs/0.12.x.md
new file mode 100644
index 0000000000..4e36f6781b
--- /dev/null
+++ b/changelogs/0.12.x.md
@@ -0,0 +1,551 @@
+# Changelog 0.12.x
+
+## 0.12.0
+
+Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
+guide and overview of the changes!
+
+### Breaking changes
+
+- **Detection of more syntax errors**
+
+ Ruff now detects version-related syntax errors, such as the use of the `match`
+ statement on Python versions before 3.10, and syntax errors emitted by
+ CPython's compiler, such as irrefutable `match` patterns before the final
+ `case` arm.
+
+- **New default Python version handling for syntax errors**
+
+ Ruff will default to the *latest* supported Python version (3.13) when
+ checking for the version-related syntax errors mentioned above to prevent
+ false positives in projects without a Python version configured. The default
+ in all other cases, like applying lint rules, is unchanged and remains at the
+ minimum supported Python version (3.9).
+
+- **Updated f-string formatting**
+
+ Ruff now formats multi-line f-strings with format specifiers to avoid adding a
+ line break after the format specifier. This addresses a change to the Python
+ grammar in version 3.13.4 that made such a line break a syntax error.
+
+- **`rust-toolchain.toml` is no longer included in source distributions**
+
+ The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
+ minimum supported Rust version (MSRV) for development and building release
+ artifacts. However, when present in source distributions, it would also cause
+ downstream package maintainers to pull in the same Rust toolchain, even if
+ their available toolchain was MSRV-compatible.
+
+### Removed Rules
+
+The following rules have been removed:
+
+- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
+ (`S320`)
+
+### Deprecated Rules
+
+The following rules have been deprecated:
+
+- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
+
+### Stabilization
+
+The following rules have been stabilized and are no longer in preview:
+
+- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
+- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
+- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
+- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
+- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
+- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
+- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
+- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
+- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
+- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
+- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
+- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
+- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
+- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
+- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
+- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
+- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
+- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
+- [`non-pep604-annotation-optional`] (`UP045`)
+- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
+- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
+- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
+
+The following behaviors have been stabilized:
+
+- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
+ addition to list literals and variables.
+- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
+- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
+ expressions to use `or` instead of an `if` expression, where possible.
+- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
+ as inline comments.
+- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
+ as well as lists and tuples of literal strings, as trusted input.
+- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
+ include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
+ plain `bool` annotations.
+- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
+ `UP007` now applies only to `typing.Union`, while
+ [`non-pep604-annotation-optional`] (`UP045`) checks for use of
+ `typing.Optional`. `UP045` has also been stabilized in this release, but you
+ may need to update existing `include`, `ignore`, or `noqa` settings to
+ accommodate this change.
+
+### Preview features
+
+- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
+- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
+
+### Bug fixes
+
+- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
+- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
+- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
+- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
+- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
+- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
+- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
+- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
+- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
+- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
+- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
+- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
+- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
+- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
+- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
+
+### Rule changes
+
+- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
+- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
+- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
+- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
+- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
+- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
+
+### Server
+
+- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
+
+### Documentation
+
+- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
+- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
+- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
+- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
+
+## 0.12.1
+
+### Preview features
+
+- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
+- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
+- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
+- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
+- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
+- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
+
+### Bug fixes
+
+- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
+- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
+- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
+- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
+- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
+- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
+- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
+- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
+- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
+- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
+- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
+- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
+- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
+- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
+- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
+- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
+- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
+- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
+- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
+- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
+- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
+- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
+
+### Rule changes
+
+- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
+- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
+- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
+- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
+- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
+- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
+- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
+- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
+- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
+- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
+- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
+- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
+- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
+- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
+- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
+- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
+
+### Server
+
+- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
+
+### Documentation
+
+- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
+- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
+- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
+- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
+- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
+- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
+- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
+- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
+
+### Other changes
+
+- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
+- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
+- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
+
+## 0.12.2
+
+### Preview features
+
+- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
+- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
+
+### Bug fixes
+
+- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
+- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
+- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
+- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
+- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
+- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
+- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
+
+### Rule changes
+
+- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
+- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
+- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
+
+### Documentation
+
+- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
+- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
+- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
+- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
+- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
+- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
+- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
+- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
+- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
+- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
+- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
+- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
+- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
+- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
+- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
+- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
+- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
+- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
+- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
+- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
+- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
+- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
+- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
+- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
+- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
+- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
+- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
+- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
+- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
+- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
+
+## 0.12.3
+
+### Preview features
+
+- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
+- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
+- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
+
+### Bug fixes
+
+- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
+- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
+- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
+- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
+
+### Documentation
+
+- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
+- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
+- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
+- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
+- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
+- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
+- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
+- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
+- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
+- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
+
+## 0.12.4
+
+### Preview features
+
+- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
+- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
+- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
+
+### Bug fixes
+
+- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
+- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
+- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
+- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
+- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
+- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
+- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
+
+### Rule changes
+
+- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
+- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
+- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
+- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
+- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
+
+### Documentation
+
+- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
+- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
+
+## 0.12.5
+
+### Preview features
+
+- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
+- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
+
+### Bug fixes
+
+- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
+- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
+- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
+- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
+
+### Rule changes
+
+- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
+
+## 0.12.6
+
+### Preview features
+
+- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
+- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
+- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
+
+### Bug fixes
+
+- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
+- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
+- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
+- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
+- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
+- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
+- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
+
+### Rule changes
+
+- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
+
+### Performance
+
+- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
+
+## 0.12.7
+
+This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
+
+## 0.12.8
+
+### Preview features
+
+- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
+
+### Bug fixes
+
+- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
+- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
+- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
+- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
+- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
+- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
+- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
+- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
+
+### Rule changes
+
+- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
+
+### Documentation
+
+- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
+- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
+- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
+- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
+
+### Other changes
+
+- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
+- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
+- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
+
+## 0.12.9
+
+### Preview features
+
+- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
+- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
+
+### Bug fixes
+
+- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
+- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
+- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
+
+### Rule changes
+
+- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
+
+### Documentation
+
+- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
+
+### Other changes
+
+- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
+
+- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
+
+- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
+
+ Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
+
+ ```diff
+ -unused.py:8:19: F401 [*] `pathlib` imported but unused
+ +F401 [*] `pathlib` imported but unused
+ + --> unused.py:8:19
+ |
+ 7 | # Unused, _not_ marked as required (due to the alias).
+ 8 | import pathlib as non_alias
+ - | ^^^^^^^^^ F401
+ + | ^^^^^^^^^
+ 9 |
+ 10 | # Unused, marked as required.
+ |
+ - = help: Remove unused import: `pathlib`
+ +help: Remove unused import: `pathlib`
+ ```
+
+ For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
+
+## 0.12.10
+
+### Preview features
+
+- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
+- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
+
+### Bug fixes
+
+- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
+- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
+- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
+
+### Rule changes
+
+- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
+- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
+
+### Documentation
+
+- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
+- Update outdated links to ([#19992](https://github.com/astral-sh/ruff/pull/19992))
+- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
+
+## 0.12.11
+
+### Preview features
+
+- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
+- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
+- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
+- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
+- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
+- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
+
+### Bug fixes
+
+- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
+- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
+- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
+- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
+
+### Rule changes
+
+- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
+
+### Documentation
+
+- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
+- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
+
+## 0.12.12
+
+### Preview features
+
+- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
+- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
+- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
+- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
+- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
+- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
+- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
+- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
+
+### Bug fixes
+
+- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
+- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
+- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
+- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
+
+### Rule changes
+
+- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
+
+### Documentation
+
+- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
+
+[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
+[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
+[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
+[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
+[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
+[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
+[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
+[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa
diff --git a/changelogs/0.13.x.md b/changelogs/0.13.x.md
new file mode 100644
index 0000000000..e242bc5478
--- /dev/null
+++ b/changelogs/0.13.x.md
@@ -0,0 +1,263 @@
+# Changelog 0.13.x
+
+## 0.13.0
+
+Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration
+guide and overview of the changes!
+
+### Breaking changes
+
+- **Several rules can now add `from __future__ import annotations` automatically**
+
+ `TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
+ `lint.future-annotations` setting is enabled. This allows the rules to move
+ more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
+ use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
+ unquote more annotations (`UP037`).
+
+- **Full module paths are now used to verify first-party modules**
+
+ Ruff now checks that the full path to a module exists on disk before
+ categorizing it as a first-party import. This change makes first-party
+ import detection more accurate, helping to avoid false positives on local
+ directories with the same name as a third-party dependency, for example. See
+ the [FAQ
+ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
+
+- **Deprecated rules must now be selected by exact rule code**
+
+ Ruff will no longer activate deprecated rules selected by their group name
+ or prefix. As noted below, the two remaining deprecated rules were also
+ removed in this release, so this won't affect any current rules, but it will
+ still affect any deprecations in the future.
+
+- **The deprecated macOS configuration directory fallback has been removed**
+
+ Ruff will no longer look for a user-level configuration file at
+ `~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
+ deprecated in v0.5 in favor of using the [XDG
+ specification](https://specifications.freedesktop.org/basedir-spec/latest/)
+ (usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
+ fallback and accompanying deprecation warning have now been removed.
+
+### Removed Rules
+
+The following rules have been removed:
+
+- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`)
+- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`)
+
+### Stabilization
+
+The following rules have been stabilized and are no longer in preview:
+
+- [`airflow-dag-no-schedule-argument`](https://docs.astral.sh/ruff/rules/airflow-dag-no-schedule-argument)
+ (`AIR002`)
+- [`airflow3-removal`](https://docs.astral.sh/ruff/rules/airflow3-removal) (`AIR301`)
+- [`airflow3-moved-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-moved-to-provider)
+ (`AIR302`)
+- [`airflow3-suggested-update`](https://docs.astral.sh/ruff/rules/airflow3-suggested-update)
+ (`AIR311`)
+- [`airflow3-suggested-to-move-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-suggested-to-move-to-provider)
+ (`AIR312`)
+- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever) (`ASYNC116`)
+- [`f-string-number-format`](https://docs.astral.sh/ruff/rules/f-string-number-format) (`FURB116`)
+- [`os-symlink`](https://docs.astral.sh/ruff/rules/os-symlink) (`PTH211`)
+- [`generic-not-last-base-class`](https://docs.astral.sh/ruff/rules/generic-not-last-base-class)
+ (`PYI059`)
+- [`redundant-none-literal`](https://docs.astral.sh/ruff/rules/redundant-none-literal) (`PYI061`)
+- [`pytest-raises-ambiguous-pattern`](https://docs.astral.sh/ruff/rules/pytest-raises-ambiguous-pattern)
+ (`RUF043`)
+- [`unused-unpacked-variable`](https://docs.astral.sh/ruff/rules/unused-unpacked-variable)
+ (`RUF059`)
+- [`useless-class-metaclass-type`](https://docs.astral.sh/ruff/rules/useless-class-metaclass-type)
+ (`UP050`)
+
+The following behaviors have been stabilized:
+
+- [`assert-raises-exception`](https://docs.astral.sh/ruff/rules/assert-raises-exception) (`B017`)
+ now checks for direct calls to `unittest.TestCase.assert_raises` and `pytest.raises` instead of
+ only the context manager forms.
+- [`missing-trailing-comma`](https://docs.astral.sh/ruff/rules/missing-trailing-comma) (`COM812`)
+ and [`prohibited-trailing-comma`](https://docs.astral.sh/ruff/rules/prohibited-trailing-comma)
+ (`COM819`) now check for trailing commas in PEP 695 type parameter lists.
+- [`raw-string-in-exception`](https://docs.astral.sh/ruff/rules/raw-string-in-exception) (`EM101`)
+ now also checks for byte strings in exception messages.
+- [`invalid-mock-access`](https://docs.astral.sh/ruff/rules/invalid-mock-access) (`PGH005`) now
+ checks for `AsyncMock` methods like `not_awaited` in addition to the synchronous variants.
+- [`useless-import-alias`](https://docs.astral.sh/ruff/rules/useless-import-alias) (`PLC0414`) no
+ longer applies to `__init__.py` files, where it conflicted with one of the suggested fixes for
+ [`unused-import`](https://docs.astral.sh/ruff/rules/unused-import) (`F401`).
+- [`bidirectional-unicode`](https://docs.astral.sh/ruff/rules/bidirectional-unicode) (`PLE2502`) now
+ also checks for U+061C (Arabic Letter Mark).
+- The fix for
+ [`multiple-with-statements`](https://docs.astral.sh/ruff/rules/multiple-with-statements)
+ (`SIM117`) is now marked as always safe.
+
+### Preview features
+
+- \[`pyupgrade`\] Enable `UP043` in stub files ([#20027](https://github.com/astral-sh/ruff/pull/20027))
+
+### Bug fixes
+
+- \[`pyupgrade`\] Apply `UP008` only when the `__class__` cell exists ([#19424](https://github.com/astral-sh/ruff/pull/19424))
+- \[`ruff`\] Fix empty f-string detection in `in-empty-collection` (`RUF060`) ([#20249](https://github.com/astral-sh/ruff/pull/20249))
+
+### Server
+
+- Add support for using uv as an alternative formatter backend ([#19665](https://github.com/astral-sh/ruff/pull/19665))
+
+### Documentation
+
+- \[`pep8-naming`\] Fix formatting of `__all__` (`N816`) ([#20301](https://github.com/astral-sh/ruff/pull/20301))
+
+## 0.13.1
+
+Released on 2025-09-18.
+
+### Preview features
+
+- \[`flake8-simplify`\] Detect unnecessary `None` default for additional key expression types (`SIM910`) ([#20343](https://github.com/astral-sh/ruff/pull/20343))
+- \[`flake8-use-pathlib`\] Add fix for `PTH123` ([#20169](https://github.com/astral-sh/ruff/pull/20169))
+- \[`flake8-use-pathlib`\] Fix `PTH101`, `PTH104`, `PTH105`, `PTH121` fixes ([#20143](https://github.com/astral-sh/ruff/pull/20143))
+- \[`flake8-use-pathlib`\] Make `PTH111` fix unsafe because it can change behavior ([#20215](https://github.com/astral-sh/ruff/pull/20215))
+- \[`pycodestyle`\] Fix `E301` to only trigger for functions immediately within a class ([#19768](https://github.com/astral-sh/ruff/pull/19768))
+- \[`refurb`\] Mark `single-item-membership-test` fix as always unsafe (`FURB171`) ([#20279](https://github.com/astral-sh/ruff/pull/20279))
+
+### Bug fixes
+
+- Handle t-strings for token-based rules and suppression comments ([#20357](https://github.com/astral-sh/ruff/pull/20357))
+- \[`flake8-bandit`\] Fix truthiness: dict-only `**` displays not truthy for `shell` (`S602`, `S604`, `S609`) ([#20177](https://github.com/astral-sh/ruff/pull/20177))
+- \[`flake8-simplify`\] Fix diagnostic to show correct method name for `str.rsplit` calls (`SIM905`) ([#20459](https://github.com/astral-sh/ruff/pull/20459))
+- \[`flynt`\] Use triple quotes for joined raw strings with newlines (`FLY002`) ([#20197](https://github.com/astral-sh/ruff/pull/20197))
+- \[`pyupgrade`\] Fix false positive when class name is shadowed by local variable (`UP008`) ([#20427](https://github.com/astral-sh/ruff/pull/20427))
+- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20327](https://github.com/astral-sh/ruff/pull/20327))
+- \[`ruff`\] Recognize t-strings, generators, and lambdas in `invalid-index-type` (`RUF016`) ([#20213](https://github.com/astral-sh/ruff/pull/20213))
+
+### Rule changes
+
+- \[`RUF102`\] Respect rule redirects in invalid rule code detection ([#20245](https://github.com/astral-sh/ruff/pull/20245))
+- \[`flake8-bugbear`\] Mark the fix for `unreliable-callable-check` as always unsafe (`B004`) ([#20318](https://github.com/astral-sh/ruff/pull/20318))
+- \[`ruff`\] Allow dataclass attribute value instantiation from nested frozen dataclass (`RUF009`) ([#20352](https://github.com/astral-sh/ruff/pull/20352))
+
+### CLI
+
+- Add fixes to `output-format=sarif` ([#20300](https://github.com/astral-sh/ruff/pull/20300))
+- Treat panics as fatal diagnostics, sort panics last ([#20258](https://github.com/astral-sh/ruff/pull/20258))
+
+### Documentation
+
+- \[`ruff`\] Add `analyze.string-imports-min-dots` to settings ([#20375](https://github.com/astral-sh/ruff/pull/20375))
+- Update README.md with Albumentations new repository URL ([#20415](https://github.com/astral-sh/ruff/pull/20415))
+
+### Other changes
+
+- Bump MSRV to Rust 1.88 ([#20470](https://github.com/astral-sh/ruff/pull/20470))
+- Enable inline noqa for multiline strings in playground ([#20442](https://github.com/astral-sh/ruff/pull/20442))
+
+### Contributors
+
+- [@chirizxc](https://github.com/chirizxc)
+- [@danparizher](https://github.com/danparizher)
+- [@IDrokin117](https://github.com/IDrokin117)
+- [@amyreese](https://github.com/amyreese)
+- [@AlexWaygood](https://github.com/AlexWaygood)
+- [@dylwil3](https://github.com/dylwil3)
+- [@njhearp](https://github.com/njhearp)
+- [@woodruffw](https://github.com/woodruffw)
+- [@dcreager](https://github.com/dcreager)
+- [@TaKO8Ki](https://github.com/TaKO8Ki)
+- [@BurntSushi](https://github.com/BurntSushi)
+- [@salahelfarissi](https://github.com/salahelfarissi)
+- [@MichaReiser](https://github.com/MichaReiser)
+
+## 0.13.2
+
+Released on 2025-09-25.
+
+### Preview features
+
+- \[`flake8-async`\] Implement `blocking-path-method` (`ASYNC240`) ([#20264](https://github.com/astral-sh/ruff/pull/20264))
+- \[`flake8-bugbear`\] Implement `map-without-explicit-strict` (`B912`) ([#20429](https://github.com/astral-sh/ruff/pull/20429))
+- \[`flake8-bultins`\] Detect class-scope builtin shadowing in decorators, default args, and attribute initializers (`A003`) ([#20178](https://github.com/astral-sh/ruff/pull/20178))
+- \[`ruff`\] Implement `logging-eager-conversion` (`RUF065`) ([#19942](https://github.com/astral-sh/ruff/pull/19942))
+- Include `.pyw` files by default when linting and formatting ([#20458](https://github.com/astral-sh/ruff/pull/20458))
+
+### Bug fixes
+
+- Deduplicate input paths ([#20105](https://github.com/astral-sh/ruff/pull/20105))
+- \[`flake8-comprehensions`\] Preserve trailing commas for single-element lists (`C409`) ([#19571](https://github.com/astral-sh/ruff/pull/19571))
+- \[`flake8-pyi`\] Avoid syntax error from conflict with `PIE790` (`PYI021`) ([#20010](https://github.com/astral-sh/ruff/pull/20010))
+- \[`flake8-simplify`\] Correct fix for positive `maxsplit` without separator (`SIM905`) ([#20056](https://github.com/astral-sh/ruff/pull/20056))
+- \[`pyupgrade`\] Fix `UP008` not to apply when `__class__` is a local variable ([#20497](https://github.com/astral-sh/ruff/pull/20497))
+- \[`ruff`\] Fix `B004` to skip invalid `hasattr`/`getattr` calls ([#20486](https://github.com/astral-sh/ruff/pull/20486))
+- \[`ruff`\] Replace `-nan` with `nan` when using the value to construct a `Decimal` (`FURB164` ) ([#20391](https://github.com/astral-sh/ruff/pull/20391))
+
+### Documentation
+
+- Add 'Finding ways to help' to CONTRIBUTING.md ([#20567](https://github.com/astral-sh/ruff/pull/20567))
+- Update import path to `ruff-wasm-web` ([#20539](https://github.com/astral-sh/ruff/pull/20539))
+- \[`flake8-bandit`\] Clarify the supported hashing functions (`S324`) ([#20534](https://github.com/astral-sh/ruff/pull/20534))
+
+### Other changes
+
+- \[`playground`\] Allow hover quick fixes to appear for overlapping diagnostics ([#20527](https://github.com/astral-sh/ruff/pull/20527))
+- \[`playground`\] Fix non‑BMP code point handling in quick fixes and markers ([#20526](https://github.com/astral-sh/ruff/pull/20526))
+
+### Contributors
+
+- [@BurntSushi](https://github.com/BurntSushi)
+- [@mtshiba](https://github.com/mtshiba)
+- [@second-ed](https://github.com/second-ed)
+- [@danparizher](https://github.com/danparizher)
+- [@ShikChen](https://github.com/ShikChen)
+- [@PieterCK](https://github.com/PieterCK)
+- [@GDYendell](https://github.com/GDYendell)
+- [@RazerM](https://github.com/RazerM)
+- [@TaKO8Ki](https://github.com/TaKO8Ki)
+- [@amyreese](https://github.com/amyreese)
+- [@ntbre](https://github.com/ntBre)
+- [@MichaReiser](https://github.com/MichaReiser)
+
+## 0.13.3
+
+Released on 2025-10-02.
+
+### Preview features
+
+- Display diffs for `ruff format --check` and add support for different output formats ([#20443](https://github.com/astral-sh/ruff/pull/20443))
+- \[`pyflakes`\] Handle some common submodule import situations for `unused-import` (`F401`) ([#20200](https://github.com/astral-sh/ruff/pull/20200))
+- \[`ruff`\] Do not flag `%r` + `repr()` combinations (`RUF065`) ([#20600](https://github.com/astral-sh/ruff/pull/20600))
+
+### Bug fixes
+
+- \[`cli`\] Add conflict between `--add-noqa` and `--diff` options ([#20642](https://github.com/astral-sh/ruff/pull/20642))
+- \[`pylint`\] Exempt required imports from `PLR0402` ([#20381](https://github.com/astral-sh/ruff/pull/20381))
+- \[`pylint`\] Fix missing `max-nested-blocks` in settings display ([#20574](https://github.com/astral-sh/ruff/pull/20574))
+- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20634](https://github.com/astral-sh/ruff/pull/20634))
+
+### Rule changes
+
+- \[`flake8-simplify`\] Improve help message clarity (`SIM105`) ([#20548](https://github.com/astral-sh/ruff/pull/20548))
+
+### Documentation
+
+- Add the *The Basics* title back to CONTRIBUTING.md ([#20624](https://github.com/astral-sh/ruff/pull/20624))
+- Fixed documentation for try_consider_else ([#20587](https://github.com/astral-sh/ruff/pull/20587))
+- \[`isort`\] Clarify dependency between `order-by-type` and `case-sensitive` settings ([#20559](https://github.com/astral-sh/ruff/pull/20559))
+- \[`pylint`\] Clarify fix safety to include left-hand hashability (`PLR6201`) ([#20518](https://github.com/astral-sh/ruff/pull/20518))
+
+### Other changes
+
+- \[`playground`\] Fix quick fixes for empty ranges in playground ([#20599](https://github.com/astral-sh/ruff/pull/20599))
+
+### Contributors
+
+- [@TaKO8Ki](https://github.com/TaKO8Ki)
+- [@ntBre](https://github.com/ntBre)
+- [@dylwil3](https://github.com/dylwil3)
+- [@MichaReiser](https://github.com/MichaReiser)
+- [@danparizher](https://github.com/danparizher)
+- [@LilMonk](https://github.com/LilMonk)
+- [@mgiovani](https://github.com/mgiovani)
+- [@IDrokin117](https://github.com/IDrokin117)
diff --git a/clippy.toml b/clippy.toml
index 539d63305b..c11a535aad 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -24,3 +24,20 @@ ignore-interior-mutability = [
# The expression is read-only.
"ruff_python_ast::hashable::HashableExpr",
]
+
+disallowed-methods = [
+ { path = "std::env::var", reason = "Use System::env_var instead in ty crates" },
+ { path = "std::env::current_dir", reason = "Use System::current_directory instead in ty crates" },
+ { path = "std::fs::read_to_string", reason = "Use System::read_to_string instead in ty crates" },
+ { path = "std::fs::metadata", reason = "Use System::path_metadata instead in ty crates" },
+ { path = "std::fs::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
+ { path = "dunce::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
+ { path = "std::fs::read_dir", reason = "Use System::read_directory instead in ty crates" },
+ { path = "std::fs::write", reason = "Use WritableSystem::write_file instead in ty crates" },
+ { path = "std::fs::create_dir_all", reason = "Use WritableSystem::create_directory_all instead in ty crates" },
+ { path = "std::fs::File::create_new", reason = "Use WritableSystem::create_new_file instead in ty crates" },
+ # Path methods that have System trait equivalents
+ { path = "std::path::Path::exists", reason = "Use System::path_exists instead in ty crates" },
+ { path = "std::path::Path::is_dir", reason = "Use System::is_directory instead in ty crates" },
+ { path = "std::path::Path::is_file", reason = "Use System::is_file instead in ty crates" },
+]
diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml
index dfe8391b2e..7bbce1f163 100644
--- a/crates/ruff/Cargo.toml
+++ b/crates/ruff/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ruff"
-version = "0.12.1"
+version = "0.14.0"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -72,6 +72,7 @@ dunce = { workspace = true }
indoc = { workspace = true }
insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { workspace = true }
+ruff_python_trivia = { workspace = true }
tempfile = { workspace = true }
test-case = { workspace = true }
@@ -85,7 +86,7 @@ dist = true
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }
-[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
+[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]
diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs
index 45db84d053..eb4bcd0a92 100644
--- a/crates/ruff/src/args.rs
+++ b/crates/ruff/src/args.rs
@@ -169,6 +169,9 @@ pub struct AnalyzeGraphCommand {
/// Attempt to detect imports from string literals.
#[clap(long)]
detect_string_imports: bool,
+ /// The minimum number of dots in a string import to consider it a valid import.
+ #[clap(long)]
+ min_dots: Option,
/// Enable preview mode. Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
@@ -413,6 +416,7 @@ pub struct CheckCommand {
conflicts_with = "stdin_filename",
conflicts_with = "watch",
conflicts_with = "fix",
+ conflicts_with = "diff",
)]
pub add_noqa: bool,
/// See the files Ruff will be run against with the current settings.
@@ -534,6 +538,14 @@ pub struct FormatCommand {
/// Exit with a non-zero status code if any files were modified via format, even if all files were formatted successfully.
#[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")]
pub exit_non_zero_on_format: bool,
+
+ /// Output serialization format for violations, when used with `--check`.
+ /// The default serialization format is "full".
+ ///
+ /// Note that this option is currently only respected in preview mode. A warning will be emitted
+ /// if this flag is used on stable.
+ #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
+ pub output_format: Option,
}
#[derive(Copy, Clone, Debug, clap::Parser)]
@@ -781,6 +793,7 @@ impl FormatCommand {
target_version: self.target_version.map(ast::PythonVersion::from),
cache_dir: self.cache_dir,
extension: self.extension,
+ output_format: self.output_format,
..ExplicitConfigOverrides::default()
};
@@ -808,6 +821,7 @@ impl AnalyzeGraphCommand {
} else {
None
},
+ string_imports_min_dots: self.min_dots,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
target_version: self.target_version.map(ast::PythonVersion::from),
..ExplicitConfigOverrides::default()
@@ -1305,6 +1319,7 @@ struct ExplicitConfigOverrides {
show_fixes: Option,
extension: Option>,
detect_string_imports: Option,
+ string_imports_min_dots: Option,
}
impl ConfigurationTransformer for ExplicitConfigOverrides {
@@ -1392,6 +1407,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
if let Some(detect_string_imports) = &self.detect_string_imports {
config.analyze.detect_string_imports = Some(*detect_string_imports);
}
+ if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
+ config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
+ }
config
}
diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs
index fdaf6b4a74..6b696b55d3 100644
--- a/crates/ruff/src/cache.rs
+++ b/crates/ruff/src/cache.rs
@@ -13,24 +13,16 @@ use itertools::Itertools;
use log::{debug, error};
use rayon::iter::ParallelIterator;
use rayon::iter::{IntoParallelIterator, ParallelBridge};
-use ruff_linter::codes::Rule;
use rustc_hash::FxHashMap;
use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
-use ruff_diagnostics::Fix;
-use ruff_linter::message::OldDiagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::{VERSION, warn_user};
use ruff_macros::CacheKey;
-use ruff_notebook::NotebookIndex;
-use ruff_source_file::SourceFileBuilder;
-use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_workspace::Settings;
use ruff_workspace::resolver::Resolver;
-use crate::diagnostics::Diagnostics;
-
/// [`Path`] that is relative to the package root in [`PackageCache`].
pub(crate) type RelativePath = Path;
/// [`PathBuf`] that is relative to the package root in [`PackageCache`].
@@ -297,13 +289,8 @@ impl Cache {
});
}
- pub(crate) fn update_lint(
- &self,
- path: RelativePathBuf,
- key: &FileCacheKey,
- data: LintCacheData,
- ) {
- self.update(path, key, ChangeData::Lint(data));
+ pub(crate) fn set_linted(&self, path: RelativePathBuf, key: &FileCacheKey, yes: bool) {
+ self.update(path, key, ChangeData::Linted(yes));
}
pub(crate) fn set_formatted(&self, path: RelativePathBuf, key: &FileCacheKey) {
@@ -338,42 +325,15 @@ pub(crate) struct FileCache {
}
impl FileCache {
- /// Convert the file cache into `Diagnostics`, using `path` as file name.
- pub(crate) fn to_diagnostics(&self, path: &Path) -> Option {
- self.data.lint.as_ref().map(|lint| {
- let diagnostics = if lint.messages.is_empty() {
- Vec::new()
- } else {
- let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
- lint.messages
- .iter()
- .map(|msg| {
- OldDiagnostic::lint(
- &msg.body,
- msg.suggestion.as_ref(),
- msg.range,
- msg.fix.clone(),
- msg.parent,
- file.clone(),
- msg.noqa_offset,
- msg.rule,
- )
- })
- .collect()
- };
- let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
- FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
- } else {
- FxHashMap::default()
- };
- Diagnostics::new(diagnostics, notebook_indexes)
- })
+ /// Return whether or not the file in the cache was linted and found to have no diagnostics.
+ pub(crate) fn linted(&self) -> bool {
+ self.data.linted
}
}
#[derive(Debug, Default, bincode::Decode, bincode::Encode)]
struct FileCacheData {
- lint: Option,
+ linted: bool,
formatted: bool,
}
@@ -409,88 +369,6 @@ pub(crate) fn init(path: &Path) -> Result<()> {
Ok(())
}
-#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)]
-pub(crate) struct LintCacheData {
- /// Imports made.
- // pub(super) imports: ImportMap,
- /// Diagnostic messages.
- pub(super) messages: Vec,
- /// Source code of the file.
- ///
- /// # Notes
- ///
- /// This will be empty if `messages` is empty.
- pub(super) source: String,
- /// Notebook index if this file is a Jupyter Notebook.
- #[bincode(with_serde)]
- pub(super) notebook_index: Option,
-}
-
-impl LintCacheData {
- pub(crate) fn from_diagnostics(
- diagnostics: &[OldDiagnostic],
- notebook_index: Option,
- ) -> Self {
- let source = if let Some(msg) = diagnostics.first() {
- msg.source_file().source_text().to_owned()
- } else {
- String::new() // No messages, no need to keep the source!
- };
-
- let messages = diagnostics
- .iter()
- // Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
- // this also serves to filter them out, but we shouldn't be caching files with syntax
- // errors anyway.
- .filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
- .map(|(rule, msg)| {
- // Make sure that all message use the same source file.
- assert_eq!(
- msg.source_file(),
- diagnostics.first().unwrap().source_file(),
- "message uses a different source file"
- );
- CacheMessage {
- rule,
- body: msg.body().to_string(),
- suggestion: msg.suggestion().map(ToString::to_string),
- range: msg.range(),
- parent: msg.parent,
- fix: msg.fix().cloned(),
- noqa_offset: msg.noqa_offset(),
- }
- })
- .collect();
-
- Self {
- messages,
- source,
- notebook_index,
- }
- }
-}
-
-/// On disk representation of a diagnostic message.
-#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)]
-pub(super) struct CacheMessage {
- /// The rule for the cached diagnostic.
- #[bincode(with_serde)]
- rule: Rule,
- /// The message body to display to the user, to explain the diagnostic.
- body: String,
- /// The message to display to the user, to explain the suggested fix.
- suggestion: Option,
- /// Range into the message's [`FileCache::source`].
- #[bincode(with_serde)]
- range: TextRange,
- #[bincode(with_serde)]
- parent: Option,
- #[bincode(with_serde)]
- fix: Option,
- #[bincode(with_serde)]
- noqa_offset: Option,
-}
-
pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>;
@@ -578,15 +456,15 @@ struct Change {
#[derive(Debug)]
enum ChangeData {
- Lint(LintCacheData),
+ Linted(bool),
Formatted,
}
impl ChangeData {
fn apply(self, data: &mut FileCacheData) {
match self {
- ChangeData::Lint(new_lint) => {
- data.lint = Some(new_lint);
+ ChangeData::Linted(yes) => {
+ data.linted = yes;
}
ChangeData::Formatted => {
data.formatted = true;
@@ -608,18 +486,17 @@ mod tests {
use anyhow::Result;
use filetime::{FileTime, set_file_mtime};
use itertools::Itertools;
- use ruff_linter::settings::LinterSettings;
use test_case::test_case;
use ruff_cache::CACHE_DIR_NAME;
- use ruff_linter::message::OldDiagnostic;
use ruff_linter::package::PackageRoot;
+ use ruff_linter::settings::LinterSettings;
use ruff_linter::settings::flags;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_python_ast::{PySourceType, PythonVersion};
use ruff_workspace::Settings;
- use crate::cache::{self, FileCache, FileCacheData, FileCacheKey};
+ use crate::cache::{self, ChangeData, FileCache, FileCacheData, FileCacheKey};
use crate::cache::{Cache, RelativePathBuf};
use crate::commands::format::{FormatCommandError, FormatMode, FormatResult, format_path};
use crate::diagnostics::{Diagnostics, lint_path};
@@ -646,7 +523,7 @@ mod tests {
assert_eq!(cache.changes.lock().unwrap().len(), 0);
let mut paths = Vec::new();
- let mut parse_errors = Vec::new();
+ let mut paths_with_diagnostics = Vec::new();
let mut expected_diagnostics = Diagnostics::default();
for entry in fs::read_dir(&package_root).unwrap() {
let entry = entry.unwrap();
@@ -670,7 +547,7 @@ mod tests {
continue;
}
- let diagnostics = lint_path(
+ let mut diagnostics = lint_path(
&path,
Some(PackageRoot::root(&package_root)),
&settings.linter,
@@ -680,8 +557,15 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
- if diagnostics.inner.iter().any(OldDiagnostic::is_syntax_error) {
- parse_errors.push(path.clone());
+ if diagnostics.inner.is_empty() {
+ // We won't load a notebook index from the cache for files without diagnostics,
+ // so remove them from `expected_diagnostics` too. This allows us to keep the
+ // full equality assertion below.
+ diagnostics
+ .notebook_indexes
+ .remove(&path.to_string_lossy().to_string());
+ } else {
+ paths_with_diagnostics.push(path.clone());
}
paths.push(path);
expected_diagnostics += diagnostics;
@@ -694,11 +578,11 @@ mod tests {
let cache = Cache::open(package_root.clone(), &settings);
assert_ne!(cache.package.files.len(), 0);
- parse_errors.sort();
+ paths_with_diagnostics.sort();
for path in &paths {
- if parse_errors.binary_search(path).is_ok() {
- continue; // We don't cache parsing errors.
+ if paths_with_diagnostics.binary_search(path).is_ok() {
+ continue; // We don't cache files with diagnostics.
}
let relative_path = cache.relative_path(path).unwrap();
@@ -732,7 +616,7 @@ mod tests {
#[test]
fn cache_adds_file_on_lint() {
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
let test_cache = TestCache::new("cache_adds_file_on_lint");
let cache = test_cache.open();
@@ -756,7 +640,7 @@ mod tests {
#[test]
fn cache_adds_files_on_lint() {
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
let test_cache = TestCache::new("cache_adds_files_on_lint");
let cache = test_cache.open();
@@ -781,6 +665,40 @@ mod tests {
cache.persist().unwrap();
}
+ #[test]
+ fn cache_does_not_add_file_on_lint_with_diagnostic() {
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+
+ let test_cache = TestCache::new("cache_does_not_add_file_on_lint_with_diagnostic");
+ let cache = test_cache.open();
+ test_cache.write_source_file("source.py", source);
+ assert_eq!(cache.changes.lock().unwrap().len(), 0);
+
+ cache.persist().unwrap();
+ let cache = test_cache.open();
+
+ let results = test_cache
+ .lint_file_with_cache("source.py", &cache)
+ .expect("Failed to lint test file");
+ assert_eq!(results.inner.len(), 1, "Expected one F822 diagnostic");
+ assert_eq!(
+ cache.changes.lock().unwrap().len(),
+ 1,
+ "Files with diagnostics still trigger change events"
+ );
+ assert!(
+ cache
+ .changes
+ .lock()
+ .unwrap()
+ .last()
+ .is_some_and(|change| matches!(change.new_data, ChangeData::Linted(false))),
+ "Files with diagnostics are marked as unlinted"
+ );
+
+ cache.persist().unwrap();
+ }
+
#[test]
fn cache_adds_files_on_format() {
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
@@ -811,7 +729,7 @@ mod tests {
#[test]
fn cache_invalidated_on_file_modified_time() {
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
let test_cache = TestCache::new("cache_invalidated_on_file_modified_time");
let cache = test_cache.open();
@@ -868,7 +786,7 @@ mod tests {
file.set_permissions(perms)
}
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
let test_cache = TestCache::new("cache_invalidated_on_permission_change");
let cache = test_cache.open();
@@ -921,7 +839,7 @@ mod tests {
);
// Now actually lint a file.
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
test_cache.write_source_file("new.py", source);
let new_path_key = RelativePathBuf::from("new.py");
assert_eq!(cache.changes.lock().unwrap().len(), 0);
@@ -944,7 +862,7 @@ mod tests {
#[test]
fn format_updates_cache_entry() {
- let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
+ let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
let test_cache = TestCache::new("format_updates_cache_entry");
let cache = test_cache.open();
@@ -978,7 +896,7 @@ mod tests {
panic!("Cache entry for `source.py` is missing.");
};
- assert!(file_cache.data.lint.is_some());
+ assert!(file_cache.data.linted);
assert!(file_cache.data.formatted);
}
@@ -1028,7 +946,7 @@ mod tests {
panic!("Cache entry for `source.py` is missing.");
};
- assert_eq!(file_cache.data.lint, None);
+ assert!(!file_cache.data.linted);
assert!(file_cache.data.formatted);
}
diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs
index 75ff28d0dc..ffd7cc2d15 100644
--- a/crates/ruff/src/commands/analyze_graph.rs
+++ b/crates/ruff/src/commands/analyze_graph.rs
@@ -102,7 +102,7 @@ pub(crate) fn analyze_graph(
// Resolve the per-file settings.
let settings = resolver.resolve(path);
- let string_imports = settings.analyze.detect_string_imports;
+ let string_imports = settings.analyze.string_imports;
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
// Skip excluded files.
diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs
index c3df35c1ed..dcdd0f9b18 100644
--- a/crates/ruff/src/commands/check.rs
+++ b/crates/ruff/src/commands/check.rs
@@ -6,18 +6,19 @@ use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use ignore::Error;
-use log::{debug, error, warn};
+use log::{debug, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
+use ruff_linter::message::create_panic_diagnostic;
use rustc_hash::FxHashMap;
+use ruff_db::diagnostic::Diagnostic;
use ruff_db::panic::catch_unwind;
-use ruff_linter::OldDiagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
-use ruff_linter::{IOError, fs, warn_user_once};
+use ruff_linter::{IOError, Violation, fs, warn_user_once};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::TextRange;
use ruff_workspace::resolver::{
@@ -129,11 +130,7 @@ pub(crate) fn check(
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
- vec![OldDiagnostic::new(
- IOError { message },
- TextRange::default(),
- &dummy,
- )],
+ vec![IOError { message }.into_diagnostic(TextRange::default(), &dummy)],
FxHashMap::default(),
)
} else {
@@ -166,7 +163,9 @@ pub(crate) fn check(
|a, b| (a.0 + b.0, a.1 + b.1),
);
- all_diagnostics.inner.sort();
+ all_diagnostics
+ .inner
+ .sort_by(Diagnostic::ruff_start_ordering);
// Store the caches.
caches.persist()?;
@@ -195,21 +194,8 @@ fn lint_path(
match result {
Ok(inner) => inner,
Err(error) => {
- let message = r"This indicates a bug in Ruff. If you could open an issue at:
-
- https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
-
-...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
-";
-
- error!(
- "{}{}{} {message}\n{error}",
- "Panicked while linting ".bold(),
- fs::relativize_path(path).bold(),
- ":".bold()
- );
-
- Ok(Diagnostics::default())
+ let diagnostic = create_panic_diagnostic(&error, Some(path));
+ Ok(Diagnostics::new(vec![diagnostic], FxHashMap::default()))
}
}
}
@@ -224,7 +210,8 @@ mod test {
use rustc_hash::FxHashMap;
use tempfile::TempDir;
- use ruff_linter::message::{Emitter, EmitterContext, TextEmitter};
+ use ruff_db::diagnostic::{DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics};
+ use ruff_linter::message::EmitterContext;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
@@ -277,18 +264,16 @@ mod test {
UnsafeFixes::Enabled,
)
.unwrap();
- let mut output = Vec::new();
- TextEmitter::default()
- .with_show_fix_status(true)
- .emit(
- &mut output,
- &diagnostics.inner,
- &EmitterContext::new(&FxHashMap::default()),
- )
- .unwrap();
-
- let messages = String::from_utf8(output).unwrap();
+ let config = DisplayDiagnosticConfig::default()
+ .format(DiagnosticFormat::Concise)
+ .hide_severity(true);
+ let messages = DisplayDiagnostics::new(
+ &EmitterContext::new(&FxHashMap::default()),
+ &config,
+ &diagnostics.inner,
+ )
+ .to_string();
insta::with_settings!({
omit_expression => true,
diff --git a/crates/ruff/src/commands/check_stdin.rs b/crates/ruff/src/commands/check_stdin.rs
index 2af7918480..f76b3ab2df 100644
--- a/crates/ruff/src/commands/check_stdin.rs
+++ b/crates/ruff/src/commands/check_stdin.rs
@@ -1,6 +1,7 @@
use std::path::Path;
use anyhow::Result;
+use ruff_db::diagnostic::Diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
@@ -52,6 +53,8 @@ pub(crate) fn check_stdin(
noqa,
fix_mode,
)?;
- diagnostics.inner.sort_unstable();
+ diagnostics
+ .inner
+ .sort_unstable_by(Diagnostic::ruff_start_ordering);
Ok(diagnostics)
}
diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs
index 1006346008..20c000a89d 100644
--- a/crates/ruff/src/commands/format.rs
+++ b/crates/ruff/src/commands/format.rs
@@ -11,13 +11,19 @@ use itertools::Itertools;
use log::{error, warn};
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
+use ruff_db::diagnostic::{
+ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, Severity, Span,
+};
+use ruff_linter::message::{EmitterContext, create_panic_diagnostic, render_diagnostics};
+use ruff_linter::settings::types::OutputFormat;
+use ruff_notebook::NotebookIndex;
use ruff_python_parser::ParseError;
-use rustc_hash::FxHashSet;
+use rustc_hash::{FxHashMap, FxHashSet};
use thiserror::Error;
use tracing::debug;
use ruff_db::panic::{PanicError, catch_unwind};
-use ruff_diagnostics::SourceMap;
+use ruff_diagnostics::{Edit, Fix, SourceMap};
use ruff_linter::fs;
use ruff_linter::logging::{DisplayParseError, LogLevel};
use ruff_linter::package::PackageRoot;
@@ -27,14 +33,15 @@ use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{FormatModuleError, QuoteStyle, format_module_source, format_range};
-use ruff_source_file::LineIndex;
+use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::FormatterSettings;
-use ruff_workspace::resolver::{ResolvedFile, Resolver, match_exclusion, python_files_in_path};
+use ruff_workspace::resolver::{
+ PyprojectConfig, ResolvedFile, Resolver, match_exclusion, python_files_in_path,
+};
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
-use crate::resolve::resolve;
use crate::{ExitStatus, resolve_default_files};
#[derive(Debug, Copy, Clone, is_macro::Is)]
@@ -63,11 +70,14 @@ impl FormatMode {
pub(crate) fn format(
cli: FormatArguments,
config_arguments: &ConfigArguments,
+ pyproject_config: &PyprojectConfig,
) -> Result {
- let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let mode = FormatMode::from_cli(&cli);
let files = resolve_default_files(cli.files, false);
- let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?;
+ let (paths, resolver) = python_files_in_path(&files, pyproject_config, config_arguments)?;
+
+ let output_format = pyproject_config.settings.output_format;
+ let preview = pyproject_config.settings.formatter.preview;
if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)");
@@ -184,17 +194,26 @@ pub(crate) fn format(
caches.persist()?;
// Report on any errors.
- errors.sort_unstable_by(|a, b| a.path().cmp(&b.path()));
+ //
+ // We only convert errors to `Diagnostic`s in `Check` mode with preview enabled, otherwise we
+ // fall back on printing simple messages.
+ if !(preview.is_enabled() && mode.is_check()) {
+ errors.sort_unstable_by(|a, b| a.path().cmp(&b.path()));
- for error in &errors {
- error!("{error}");
+ for error in &errors {
+ error!("{error}");
+ }
}
let results = FormatResults::new(results.as_slice(), mode);
match mode {
FormatMode::Write => {}
FormatMode::Check => {
- results.write_changed(&mut stdout().lock())?;
+ if preview.is_enabled() {
+ results.write_changed_preview(&mut stdout().lock(), output_format, &errors)?;
+ } else {
+ results.write_changed(&mut stdout().lock())?;
+ }
}
FormatMode::Diff => {
results.write_diff(&mut stdout().lock())?;
@@ -206,7 +225,7 @@ pub(crate) fn format(
if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?;
- } else {
+ } else if !preview.is_enabled() || output_format.is_human_readable() {
results.write_summary(&mut stdout().lock())?;
}
}
@@ -295,8 +314,7 @@ pub(crate) fn format_path(
FormatResult::Formatted
}
- FormatMode::Check => FormatResult::Formatted,
- FormatMode::Diff => FormatResult::Diff {
+ FormatMode::Check | FormatMode::Diff => FormatResult::Diff {
unformatted,
formatted,
},
@@ -329,7 +347,7 @@ pub(crate) enum FormattedSource {
impl From for FormatResult {
fn from(value: FormattedSource) -> Self {
match value {
- FormattedSource::Formatted(_) => FormatResult::Formatted,
+ FormattedSource::Formatted { .. } => FormatResult::Formatted,
FormattedSource::Unchanged => FormatResult::Unchanged,
}
}
@@ -477,10 +495,10 @@ pub(crate) fn format_source(
/// The result of an individual formatting operation.
#[derive(Debug, Clone, is_macro::Is)]
pub(crate) enum FormatResult {
- /// The file was formatted.
+ /// The file was formatted and written back to disk.
Formatted,
- /// The file was formatted, [`SourceKind`] contains the formatted code
+ /// The file needs to be formatted, as the `formatted` and `unformatted` contents differ.
Diff {
unformatted: SourceKind,
formatted: SourceKind,
@@ -552,7 +570,7 @@ impl<'a> FormatResults<'a> {
.results
.iter()
.filter_map(|result| {
- if result.result.is_formatted() {
+ if result.result.is_diff() {
Some(result.path.as_path())
} else {
None
@@ -566,6 +584,30 @@ impl<'a> FormatResults<'a> {
Ok(())
}
+ /// Write a list of the files that would be changed and any errors to the given writer.
+ fn write_changed_preview(
+ &self,
+ f: &mut impl Write,
+ output_format: OutputFormat,
+ errors: &[FormatCommandError],
+ ) -> io::Result<()> {
+ let mut notebook_index = FxHashMap::default();
+ let diagnostics: Vec<_> = errors
+ .iter()
+ .map(Diagnostic::from)
+ .chain(self.to_diagnostics(&mut notebook_index))
+ .sorted_unstable_by(Diagnostic::ruff_start_ordering)
+ .collect();
+
+ let context = EmitterContext::new(¬ebook_index);
+ let config = DisplayDiagnosticConfig::default()
+ .hide_severity(true)
+ .show_fix_diff(true)
+ .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize());
+
+ render_diagnostics(f, output_format, config, &context, &diagnostics)
+ }
+
/// Write a summary of the formatting results to the given writer.
fn write_summary(&self, f: &mut impl Write) -> io::Result<()> {
// Compute the number of changed and unchanged files.
@@ -628,6 +670,155 @@ impl<'a> FormatResults<'a> {
Ok(())
}
}
+
+ /// Convert formatted files into [`Diagnostic`]s.
+ fn to_diagnostics(
+ &self,
+ notebook_index: &mut FxHashMap,
+ ) -> impl Iterator- {
+ /// The number of unmodified context lines rendered in diffs.
+ ///
+ /// Note that this should be kept in sync with the argument to `TextDiff::grouped_ops` in
+ /// the diff rendering in `ruff_db` (currently 3). The `similar` crate uses two times that
+ /// argument as a cutoff for rendering unmodified lines.
+ const CONTEXT_LINES: u32 = 6;
+
+ self.results.iter().filter_map(|result| {
+ let (unformatted, formatted) = match &result.result {
+ FormatResult::Skipped | FormatResult::Unchanged => return None,
+ FormatResult::Diff {
+ unformatted,
+ formatted,
+ } => (unformatted, formatted),
+ FormatResult::Formatted => {
+ debug_assert!(
+ false,
+ "Expected `FormatResult::Diff` for changed files in check mode"
+ );
+ return None;
+ }
+ };
+
+ let mut diagnostic = Diagnostic::new(
+ DiagnosticId::Unformatted,
+ Severity::Error,
+ "File would be reformatted",
+ );
+
+ // Locate the first and last characters that differ to use as the diagnostic
+ // range and to narrow the `Edit` range.
+ let modified_range = ModifiedRange::new(unformatted, formatted);
+
+ let path = result.path.to_string_lossy();
+ // For scripts, this is a single `Edit` using the `ModifiedRange` above, but notebook
+ // edits must be split by cell in order to render them as diffs.
+ //
+ // We also attempt to estimate the line number width for aligning the
+ // annotate-snippets header. This is only an estimate because we don't actually know
+ // if the maximum line number present in the document will be rendered as part of
+ // the diff, either as a changed line or as an unchanged context line. For
+ // notebooks, we refine our estimate by checking the number of lines in each cell
+ // individually, otherwise we could use `formatted.source_code().count_lines(...)`
+ // in both cases.
+ let (fix, line_count) = if let SourceKind::IpyNotebook(formatted) = formatted
+ && let SourceKind::IpyNotebook(unformatted) = unformatted
+ {
+ notebook_index.insert(path.to_string(), unformatted.index().clone());
+
+ let mut edits = formatted
+ .cell_offsets()
+ .ranges()
+ .zip(unformatted.cell_offsets().ranges())
+ .filter_map(|(formatted_range, unformatted_range)| {
+ // Filter out cells that weren't modified. We use `intersect` instead of
+ // `contains_range` because the full modified range might start or end in
+ // the middle of a cell:
+ //
+ // ```
+ // | cell 1 | cell 2 | cell 3 |
+ // |----------------| modified range
+ // ```
+ //
+ // The intersection will be `Some` for all three cells in this case.
+ if modified_range
+ .unformatted
+ .intersect(unformatted_range)
+ .is_some()
+ {
+ let formatted = &formatted.source_code()[formatted_range];
+ let edit = if formatted.is_empty() {
+ Edit::range_deletion(unformatted_range)
+ } else {
+ Edit::range_replacement(formatted.to_string(), unformatted_range)
+ };
+ Some(edit)
+ } else {
+ None
+ }
+ });
+
+ let fix = Fix::safe_edits(
+ edits
+ .next()
+ .expect("Formatted files must have at least one edit"),
+ edits,
+ );
+ let source = formatted.source_code();
+ let line_count = formatted
+ .cell_offsets()
+ .ranges()
+ .filter_map(|range| {
+ if modified_range.formatted.contains_range(range) {
+ Some(source.count_lines(range))
+ } else {
+ None
+ }
+ })
+ .max()
+ .unwrap_or_default();
+ (fix, line_count)
+ } else {
+ let formatted_code = &formatted.source_code()[modified_range.formatted];
+ let edit = if formatted_code.is_empty() {
+ Edit::range_deletion(modified_range.unformatted)
+ } else {
+ Edit::range_replacement(formatted_code.to_string(), modified_range.unformatted)
+ };
+ let fix = Fix::safe_edit(edit);
+ let line_count = formatted
+ .source_code()
+ .count_lines(TextRange::up_to(modified_range.formatted.end()));
+ (fix, line_count)
+ };
+
+ let source_file = SourceFileBuilder::new(path, unformatted.source_code()).finish();
+ let span = Span::from(source_file).with_range(modified_range.unformatted);
+ let mut annotation = Annotation::primary(span);
+ annotation.hide_snippet(true);
+ diagnostic.annotate(annotation);
+ diagnostic.set_fix(fix);
+
+ // TODO(brent) this offset is a hack to get the header of the diagnostic message, which
+ // is rendered by our fork of `annotate-snippets`, to align with our manually-rendered
+ // diff. `annotate-snippets` computes the alignment of the arrow in the header based on
+ // the maximum line number width in its rendered snippet. However, we don't have a
+ // reasonable range to underline in an annotation, so we don't send `annotate-snippets`
+ // a snippet to measure. If we commit to staying on our fork, a more robust way of
+ // handling this would be to move the diff rendering in
+ // `ruff_db::diagnostic::render::full` into `annotate-snippets`, likely as another
+ // `DisplayLine` variant and update the `lineno_width` calculation in
+ // `DisplayList::fmt`. That would handle this offset "automatically."
+ let line_count = (line_count + CONTEXT_LINES).min(
+ formatted
+ .source_code()
+ .count_lines(TextRange::up_to(formatted.source_code().text_len())),
+ );
+ let lines = OneIndexed::new(line_count as usize).unwrap_or_default();
+ diagnostic.set_header_offset(lines.digits().get());
+
+ Some(diagnostic)
+ })
+ }
}
/// An error that can occur while formatting a set of files.
@@ -639,7 +830,6 @@ pub(crate) enum FormatCommandError {
Read(Option, SourceError),
Format(Option, FormatModuleError),
Write(Option, SourceError),
- Diff(Option, io::Error),
RangeFormatNotebook(Option),
}
@@ -658,12 +848,65 @@ impl FormatCommandError {
| Self::Read(path, _)
| Self::Format(path, _)
| Self::Write(path, _)
- | Self::Diff(path, _)
| Self::RangeFormatNotebook(path) => path.as_deref(),
}
}
}
+impl From<&FormatCommandError> for Diagnostic {
+ fn from(error: &FormatCommandError) -> Self {
+ let annotation = error.path().map(|path| {
+ let file = SourceFileBuilder::new(path.to_string_lossy(), "").finish();
+ let span = Span::from(file);
+ let mut annotation = Annotation::primary(span);
+ annotation.hide_snippet(true);
+ annotation
+ });
+
+ let mut diagnostic = match error {
+ FormatCommandError::Ignore(error) => {
+ Diagnostic::new(DiagnosticId::Io, Severity::Error, error)
+ }
+ FormatCommandError::Parse(display_parse_error) => Diagnostic::new(
+ DiagnosticId::InvalidSyntax,
+ Severity::Error,
+ &display_parse_error.error().error,
+ ),
+ FormatCommandError::Panic(path, panic_error) => {
+ return create_panic_diagnostic(panic_error, path.as_deref());
+ }
+ FormatCommandError::Read(_, source_error)
+ | FormatCommandError::Write(_, source_error) => {
+ Diagnostic::new(DiagnosticId::Io, Severity::Error, source_error)
+ }
+ FormatCommandError::Format(_, format_module_error) => match format_module_error {
+ FormatModuleError::ParseError(parse_error) => Diagnostic::new(
+ DiagnosticId::InternalError,
+ Severity::Error,
+ &parse_error.error,
+ ),
+ FormatModuleError::FormatError(format_error) => {
+ Diagnostic::new(DiagnosticId::InternalError, Severity::Error, format_error)
+ }
+ FormatModuleError::PrintError(print_error) => {
+ Diagnostic::new(DiagnosticId::InternalError, Severity::Error, print_error)
+ }
+ },
+ FormatCommandError::RangeFormatNotebook(_) => Diagnostic::new(
+ DiagnosticId::InvalidCliOption,
+ Severity::Error,
+ "Range formatting isn't supported for notebooks.",
+ ),
+ };
+
+ if let Some(annotation) = annotation {
+ diagnostic.annotate(annotation);
+ }
+
+ diagnostic
+ }
+}
+
impl Display for FormatCommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
@@ -731,23 +974,6 @@ impl Display for FormatCommandError {
write!(f, "{header} {err}", header = "Failed to format:".bold())
}
}
- Self::Diff(path, err) => {
- if let Some(path) = path {
- write!(
- f,
- "{}{}{} {err}",
- "Failed to generate diff for ".bold(),
- fs::relativize_path(path).bold(),
- ":".bold()
- )
- } else {
- write!(
- f,
- "{header} {err}",
- header = "Failed to generate diff:".bold(),
- )
- }
- }
Self::RangeFormatNotebook(path) => {
if let Some(path) = path {
write!(
@@ -792,6 +1018,54 @@ impl Display for FormatCommandError {
}
}
+#[derive(Debug)]
+struct ModifiedRange {
+ unformatted: TextRange,
+ formatted: TextRange,
+}
+
+impl ModifiedRange {
+ /// Determine the range that differs between `unformatted` and `formatted`.
+ ///
+ /// If the two inputs are equal, the returned ranges will be empty.
+ fn new(unformatted: &SourceKind, formatted: &SourceKind) -> Self {
+ let unformatted = unformatted.source_code();
+ let formatted = formatted.source_code();
+
+ let mut prefix_length = TextSize::ZERO;
+ for (unformatted, formatted) in unformatted.chars().zip(formatted.chars()) {
+ if unformatted != formatted {
+ break;
+ }
+ prefix_length += unformatted.text_len();
+ }
+
+ // For the ends of the ranges, track the length of the common suffix and then subtract that
+ // from each total text length. Unlike for `start`, the character offsets are very unlikely
+ // to be equal, so they need to be treated separately.
+ let mut suffix_length = TextSize::ZERO;
+ for (old, new) in unformatted[prefix_length.to_usize()..]
+ .chars()
+ .rev()
+ .zip(formatted[prefix_length.to_usize()..].chars().rev())
+ {
+ if old != new {
+ break;
+ }
+ suffix_length += old.text_len();
+ }
+
+ let unformatted_range =
+ TextRange::new(prefix_length, unformatted.text_len() - suffix_length);
+ let formatted_range = TextRange::new(prefix_length, formatted.text_len() - suffix_length);
+
+ Self {
+ unformatted: unformatted_range,
+ formatted: formatted_range,
+ }
+ }
+}
+
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
// First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default();
@@ -963,3 +1237,144 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use std::io;
+ use std::ops::Range;
+ use std::path::PathBuf;
+
+ use ignore::Error;
+ use insta::assert_snapshot;
+
+ use ruff_db::panic::catch_unwind;
+ use ruff_linter::logging::DisplayParseError;
+ use ruff_linter::source_kind::{SourceError, SourceKind};
+ use ruff_python_formatter::FormatModuleError;
+ use ruff_python_parser::{ParseError, ParseErrorType};
+ use ruff_text_size::{TextRange, TextSize};
+ use test_case::test_case;
+
+ use crate::commands::format::{FormatCommandError, FormatMode, FormatResults, ModifiedRange};
+
+ #[test]
+ fn error_diagnostics() -> anyhow::Result<()> {
+ let path = PathBuf::from("test.py");
+ let source_kind = SourceKind::Python("1".to_string());
+
+ let panic_error = catch_unwind(|| {
+ panic!("Test panic for FormatCommandError");
+ })
+ .unwrap_err();
+
+ let errors = [
+ FormatCommandError::Ignore(Error::WithPath {
+ path: path.clone(),
+ err: Box::new(Error::Io(io::Error::new(
+ io::ErrorKind::PermissionDenied,
+ "Permission denied",
+ ))),
+ }),
+ FormatCommandError::Parse(DisplayParseError::from_source_kind(
+ ParseError {
+ error: ParseErrorType::UnexpectedIndentation,
+ location: TextRange::default(),
+ },
+ Some(path.clone()),
+ &source_kind,
+ )),
+ FormatCommandError::Panic(Some(path.clone()), Box::new(panic_error)),
+ FormatCommandError::Read(
+ Some(path.clone()),
+ SourceError::Io(io::Error::new(io::ErrorKind::NotFound, "File not found")),
+ ),
+ FormatCommandError::Format(
+ Some(path.clone()),
+ FormatModuleError::ParseError(ParseError {
+ error: ParseErrorType::EmptySlice,
+ location: TextRange::default(),
+ }),
+ ),
+ FormatCommandError::Write(
+ Some(path.clone()),
+ SourceError::Io(io::Error::new(
+ io::ErrorKind::PermissionDenied,
+ "Cannot write to file",
+ )),
+ ),
+ FormatCommandError::RangeFormatNotebook(Some(path)),
+ ];
+
+ let results = FormatResults::new(&[], FormatMode::Check);
+ let mut buf = Vec::new();
+ results.write_changed_preview(
+ &mut buf,
+ ruff_linter::settings::types::OutputFormat::Full,
+ &errors,
+ )?;
+
+ let mut settings = insta::Settings::clone_current();
+ settings.add_filter(r"(Panicked at) [^:]+:\d+:\d+", "$1 ");
+ let _s = settings.bind_to_scope();
+
+ assert_snapshot!(str::from_utf8(&buf)?, @r"
+ io: test.py: Permission denied
+ --> test.py:1:1
+
+ invalid-syntax: Unexpected indentation
+ --> test.py:1:1
+
+ io: File not found
+ --> test.py:1:1
+
+ internal-error: Expected index or slice expression
+ --> test.py:1:1
+
+ io: Cannot write to file
+ --> test.py:1:1
+
+ invalid-cli-option: Range formatting isn't supported for notebooks.
+ --> test.py:1:1
+
+ panic: Panicked at when checking `test.py`: `Test panic for FormatCommandError`
+ --> test.py:1:1
+ info: This indicates a bug in Ruff.
+ info: If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bpanic%5D, we'd be very appreciative!
+ info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information
+ ");
+
+ Ok(())
+ }
+
+ #[test_case("abcdef", "abcXYdef", 3..3, 3..5; "insertion")]
+ #[test_case("abcXYdef", "abcdef", 3..5, 3..3; "deletion")]
+ #[test_case("abcXdef", "abcYdef", 3..4, 3..4; "modification")]
+ #[test_case("abc", "abcX", 3..3, 3..4; "strict_prefix")]
+ #[test_case("", "", 0..0, 0..0; "empty")]
+ #[test_case("abc", "abc", 3..3, 3..3; "equal")]
+ fn modified_range(
+ unformatted: &str,
+ formatted: &str,
+ expect_unformatted: Range,
+ expect_formatted: Range,
+ ) {
+ let mr = ModifiedRange::new(
+ &SourceKind::Python(unformatted.to_string()),
+ &SourceKind::Python(formatted.to_string()),
+ );
+ assert_eq!(
+ mr.unformatted,
+ TextRange::new(
+ TextSize::new(expect_unformatted.start),
+ TextSize::new(expect_unformatted.end)
+ )
+ );
+ assert_eq!(
+ mr.formatted,
+ TextRange::new(
+ TextSize::new(expect_formatted.start),
+ TextSize::new(expect_formatted.end)
+ )
+ );
+ }
+}
diff --git a/crates/ruff/src/commands/format_stdin.rs b/crates/ruff/src/commands/format_stdin.rs
index f5b8ea6c60..015f32fba9 100644
--- a/crates/ruff/src/commands/format_stdin.rs
+++ b/crates/ruff/src/commands/format_stdin.rs
@@ -4,10 +4,10 @@ use std::path::Path;
use anyhow::Result;
use log::error;
-use ruff_linter::source_kind::SourceKind;
+use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::FormatterSettings;
-use ruff_workspace::resolver::{Resolver, match_exclusion, python_file_at_path};
+use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, python_file_at_path};
use crate::ExitStatus;
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
@@ -15,17 +15,15 @@ use crate::commands::format::{
FormatCommandError, FormatMode, FormatResult, FormattedSource, format_source,
warn_incompatible_formatter_settings,
};
-use crate::resolve::resolve;
use crate::stdin::{parrot_stdin, read_from_stdin};
/// Run the formatter over a single file, read from `stdin`.
pub(crate) fn format_stdin(
cli: &FormatArguments,
config_arguments: &ConfigArguments,
+ pyproject_config: &PyprojectConfig,
) -> Result {
- let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
-
- let mut resolver = Resolver::new(&pyproject_config);
+ let mut resolver = Resolver::new(pyproject_config);
warn_incompatible_formatter_settings(&resolver);
let mode = FormatMode::from_cli(cli);
@@ -124,7 +122,9 @@ fn format_source_code(
"{}",
source_kind.diff(formatted, path).unwrap()
)
- .map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
+ .map_err(|err| {
+ FormatCommandError::Write(path.map(Path::to_path_buf), SourceError::Io(err))
+ })?;
}
},
FormattedSource::Unchanged => {
diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs
index f2784d36a6..3376133ed7 100644
--- a/crates/ruff/src/diagnostics.rs
+++ b/crates/ruff/src/diagnostics.rs
@@ -10,35 +10,41 @@ use std::path::Path;
use anyhow::{Context, Result};
use colored::Colorize;
use log::{debug, warn};
-use rustc_hash::FxHashMap;
-
-use ruff_linter::OldDiagnostic;
+use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
+use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
use ruff_linter::source_kind::{SourceError, SourceKind};
-use ruff_linter::{IOError, fs};
-use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
+use ruff_linter::{IOError, Violation, fs};
+use ruff_notebook::{NotebookError, NotebookIndex};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::TextRange;
use ruff_workspace::Settings;
+use rustc_hash::FxHashMap;
-use crate::cache::{Cache, FileCacheKey, LintCacheData};
+use crate::cache::{Cache, FileCache, FileCacheKey};
+/// A collection of [`Diagnostic`]s and additional information needed to render them.
+///
+/// Note that `notebook_indexes` may be empty if there are no diagnostics because the
+/// `NotebookIndex` isn't cached in this case. This isn't a problem for any current uses as of
+/// 2025-08-12, which are all related to diagnostic rendering, but could be surprising if used
+/// differently in the future.
#[derive(Debug, Default, PartialEq)]
pub(crate) struct Diagnostics {
- pub(crate) inner: Vec,
+ pub(crate) inner: Vec,
pub(crate) fixed: FixMap,
pub(crate) notebook_indexes: FxHashMap,
}
impl Diagnostics {
pub(crate) fn new(
- diagnostics: Vec,
+ diagnostics: Vec,
notebook_indexes: FxHashMap,
) -> Self {
Self {
@@ -62,13 +68,12 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
- vec![OldDiagnostic::new(
+ vec![
IOError {
message: err.to_string(),
- },
- TextRange::default(),
- &source_file,
- )],
+ }
+ .into_diagnostic(TextRange::default(), &source_file),
+ ],
FxHashMap::default(),
)
} else {
@@ -98,10 +103,10 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
- vec![OldDiagnostic::syntax_error(
+ vec![create_syntax_error_diagnostic(
+ dummy,
err,
TextRange::default(),
- dummy,
)],
FxHashMap::default(),
)
@@ -194,19 +199,9 @@ pub(crate) fn lint_path(
let cache_key = FileCacheKey::from_path(path).context("Failed to create cache key")?;
let cached_diagnostics = cache
.get(relative_path, &cache_key)
- .and_then(|entry| entry.to_diagnostics(path));
- if let Some(diagnostics) = cached_diagnostics {
- // `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
- // and writing the diff to stdout, respectively). If a file has diagnostics, we
- // need to avoid reading from and writing to the cache in these modes.
- if match fix_mode {
- flags::FixMode::Generate => true,
- flags::FixMode::Apply | flags::FixMode::Diff => {
- diagnostics.inner.is_empty() && diagnostics.fixed.is_empty()
- }
- } {
- return Ok(diagnostics);
- }
+ .is_some_and(FileCache::linted);
+ if cached_diagnostics {
+ return Ok(Diagnostics::default());
}
// Stash the file metadata for later so when we update the cache it reflects the prerun
@@ -323,31 +318,21 @@ pub(crate) fn lint_path(
(result, transformed, fixed)
};
- let has_error = result.has_syntax_errors();
let diagnostics = result.diagnostics;
if let Some((cache, relative_path, key)) = caching {
- // We don't cache parsing errors.
- if !has_error {
- // `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
- // and writing the diff to stdout, respectively). If a file has diagnostics, we
- // need to avoid reading from and writing to the cache in these modes.
- if match fix_mode {
- flags::FixMode::Generate => true,
- flags::FixMode::Apply | flags::FixMode::Diff => {
- diagnostics.is_empty() && fixed.is_empty()
- }
- } {
- cache.update_lint(
- relative_path.to_owned(),
- &key,
- LintCacheData::from_diagnostics(
- &diagnostics,
- transformed.as_ipy_notebook().map(Notebook::index).cloned(),
- ),
- );
- }
- }
+ // `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
+ // and writing the diff to stdout, respectively). If a file has diagnostics
+ // with fixes, we need to avoid reading from and writing to the cache in these
+ // modes.
+ let use_fixes = match fix_mode {
+ flags::FixMode::Generate => true,
+ flags::FixMode::Apply | flags::FixMode::Diff => fixed.is_empty(),
+ };
+
+ // We don't cache files with diagnostics.
+ let linted = diagnostics.is_empty() && use_fixes;
+ cache.set_linted(relative_path.to_owned(), &key, linted);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs
index bd5facff5c..3bd457de8c 100644
--- a/crates/ruff/src/lib.rs
+++ b/crates/ruff/src/lib.rs
@@ -9,10 +9,11 @@ use std::sync::mpsc::channel;
use anyhow::Result;
use clap::CommandFactory;
use colored::Colorize;
-use log::warn;
+use log::{error, warn};
use notify::{RecursiveMode, Watcher, recommended_watcher};
use args::{GlobalConfigArgs, ServerCommand};
+use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_linter::logging::{LogLevel, set_up_logging};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::OutputFormat;
@@ -131,6 +132,7 @@ pub fn run(
}: Args,
) -> Result {
{
+ ruff_db::set_program_version(crate::version::version().to_string()).unwrap();
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
#[expect(clippy::print_stderr)]
@@ -203,12 +205,18 @@ pub fn run(
}
fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result {
+ let cli_output_format_set = args.output_format.is_some();
let (cli, config_arguments) = args.partition(global_options)?;
-
+ let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
+ if cli_output_format_set && !pyproject_config.settings.formatter.preview.is_enabled() {
+ warn_user_once!(
+ "The --output-format flag for the formatter is unstable and requires preview mode to use."
+ );
+ }
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
- commands::format_stdin::format_stdin(&cli, &config_arguments)
+ commands::format_stdin::format_stdin(&cli, &config_arguments, &pyproject_config)
} else {
- commands::format::format(cli, &config_arguments)
+ commands::format::format(cli, &config_arguments, &pyproject_config)
}
}
@@ -439,10 +447,31 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result ExitCode {
}
let args = wild::args_os();
- let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
+ let args = match argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
+ .context("Failed to read CLI arguments from files")
+ {
+ Ok(args) => args,
+ Err(err) => return report_error(&err),
+ };
let args = Args::parse_from(args);
match run(args) {
Ok(code) => code.into(),
- Err(err) => {
- {
- // Exit "gracefully" on broken pipe errors.
- //
- // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
- for cause in err.chain() {
- if let Some(ioerr) = cause.downcast_ref::() {
- if ioerr.kind() == std::io::ErrorKind::BrokenPipe {
- return ExitCode::from(0);
- }
- }
- }
-
- // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
- let mut stderr = std::io::stderr().lock();
-
- // This communicates that this isn't a linter error but ruff itself hard-errored for
- // some reason (e.g. failed to resolve the configuration)
- writeln!(stderr, "{}", "ruff failed".red().bold()).ok();
- // Currently we generally only see one error, but e.g. with io errors when resolving
- // the configuration it is help to chain errors ("resolving configuration failed" ->
- // "failed to read file: subdir/pyproject.toml")
- for cause in err.chain() {
- writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
- }
- }
- ExitStatus::Error.into()
- }
+ Err(err) => report_error(&err),
}
}
+
+fn report_error(err: &anyhow::Error) -> ExitCode {
+ {
+ // Exit "gracefully" on broken pipe errors.
+ //
+ // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
+ for cause in err.chain() {
+ if let Some(ioerr) = cause.downcast_ref::() {
+ if ioerr.kind() == std::io::ErrorKind::BrokenPipe {
+ return ExitCode::from(0);
+ }
+ }
+ }
+
+ // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
+ let mut stderr = std::io::stderr().lock();
+
+ // This communicates that this isn't a linter error but ruff itself hard-errored for
+ // some reason (e.g. failed to resolve the configuration)
+ writeln!(stderr, "{}", "ruff failed".red().bold()).ok();
+ // Currently we generally only see one error, but e.g. with io errors when resolving
+ // the configuration it is help to chain errors ("resolving configuration failed" ->
+ // "failed to read file: subdir/pyproject.toml")
+ for cause in err.chain() {
+ writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
+ }
+ }
+ ExitStatus::Error.into()
+}
diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs
index 8052915cfb..5c1b1d0e6a 100644
--- a/crates/ruff/src/printer.rs
+++ b/crates/ruff/src/printer.rs
@@ -9,13 +9,12 @@ use itertools::{Itertools, iterate};
use ruff_linter::linter::FixTable;
use serde::Serialize;
+use ruff_db::diagnostic::{
+ Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
+};
use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
-use ruff_linter::message::{
- AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
- JsonEmitter, JsonLinesEmitter, JunitEmitter, OldDiagnostic, PylintEmitter, RdjsonEmitter,
- SarifEmitter, SecondaryCode, TextEmitter,
-};
+use ruff_linter::message::{EmitterContext, render_diagnostics};
use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
@@ -29,8 +28,6 @@ bitflags! {
const SHOW_VIOLATIONS = 1 << 0;
/// Whether to show a summary of the fixed violations when emitting diagnostics.
const SHOW_FIX_SUMMARY = 1 << 1;
- /// Whether to show a diff of each fixed violation when emitting diagnostics.
- const SHOW_FIX_DIFF = 1 << 2;
}
}
@@ -201,6 +198,7 @@ impl Printer {
&self,
diagnostics: &Diagnostics,
writer: &mut dyn Write,
+ preview: bool,
) -> Result<()> {
if matches!(self.log_level, LogLevel::Silent) {
return Ok(());
@@ -226,67 +224,28 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
- match self.format {
- OutputFormat::Json => {
- JsonEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Rdjson => {
- RdjsonEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::JsonLines => {
- JsonLinesEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Junit => {
- JunitEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Concise | OutputFormat::Full => {
- TextEmitter::default()
- .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
- .with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
- .with_show_source(self.format == OutputFormat::Full)
- .with_unsafe_fixes(self.unsafe_fixes)
- .emit(writer, &diagnostics.inner, &context)?;
+ let config = DisplayDiagnosticConfig::default()
+ .preview(preview)
+ .hide_severity(true)
+ .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
+ .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
+ .with_fix_applicability(self.unsafe_fixes.required_applicability())
+ .show_fix_diff(preview);
- if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
- if !diagnostics.fixed.is_empty() {
- writeln!(writer)?;
- print_fix_summary(writer, &diagnostics.fixed)?;
- writeln!(writer)?;
- }
+ render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?;
+
+ if matches!(
+ self.format,
+ OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped
+ ) {
+ if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
+ if !diagnostics.fixed.is_empty() {
+ writeln!(writer)?;
+ print_fix_summary(writer, &diagnostics.fixed)?;
+ writeln!(writer)?;
}
-
- self.write_summary_text(writer, diagnostics)?;
- }
- OutputFormat::Grouped => {
- GroupedEmitter::default()
- .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
- .with_unsafe_fixes(self.unsafe_fixes)
- .emit(writer, &diagnostics.inner, &context)?;
-
- if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
- if !diagnostics.fixed.is_empty() {
- writeln!(writer)?;
- print_fix_summary(writer, &diagnostics.fixed)?;
- writeln!(writer)?;
- }
- }
- self.write_summary_text(writer, diagnostics)?;
- }
- OutputFormat::Github => {
- GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Gitlab => {
- GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Pylint => {
- PylintEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Azure => {
- AzureEmitter.emit(writer, &diagnostics.inner, &context)?;
- }
- OutputFormat::Sarif => {
- SarifEmitter.emit(writer, &diagnostics.inner, &context)?;
}
+ self.write_summary_text(writer, diagnostics)?;
}
writer.flush()?;
@@ -306,8 +265,7 @@ impl Printer {
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
- |mut acc: Vec<((Option<&SecondaryCode>, &OldDiagnostic), usize)>,
- (code, message)| {
+ |mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
@@ -431,11 +389,22 @@ impl Printer {
}
let context = EmitterContext::new(&diagnostics.notebook_indexes);
- TextEmitter::default()
+ let format = if preview {
+ DiagnosticFormat::Full
+ } else {
+ DiagnosticFormat::Concise
+ };
+ let config = DisplayDiagnosticConfig::default()
+ .hide_severity(true)
+ .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
- .with_show_source(preview)
- .with_unsafe_fixes(self.unsafe_fixes)
- .emit(writer, &diagnostics.inner, &context)?;
+ .format(format)
+ .with_fix_applicability(self.unsafe_fixes.required_applicability());
+ write!(
+ writer,
+ "{}",
+ DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
+ )?;
}
writer.flush()?;
diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs
index 485aeb511b..2c300029ea 100644
--- a/crates/ruff/tests/analyze_graph.rs
+++ b/crates/ruff/tests/analyze_graph.rs
@@ -57,33 +57,40 @@ fn dependencies() -> Result<()> {
.write_str(indoc::indoc! {r#"
def f(): pass
"#})?;
+ root.child("ruff")
+ .child("e.pyi")
+ .write_str(indoc::indoc! {r#"
+ def f() -> None: ...
+ "#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
- assert_cmd_snapshot!(command().current_dir(&root), @r###"
- success: true
- exit_code: 0
- ----- stdout -----
- {
- "ruff/__init__.py": [],
- "ruff/a.py": [
- "ruff/b.py"
- ],
- "ruff/b.py": [
- "ruff/c.py"
- ],
- "ruff/c.py": [
- "ruff/d.py"
- ],
- "ruff/d.py": [
- "ruff/e.py"
- ],
- "ruff/e.py": []
- }
+ assert_cmd_snapshot!(command().current_dir(&root), @r#"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ {
+ "ruff/__init__.py": [],
+ "ruff/a.py": [
+ "ruff/b.py"
+ ],
+ "ruff/b.py": [
+ "ruff/c.py"
+ ],
+ "ruff/c.py": [
+ "ruff/d.py"
+ ],
+ "ruff/d.py": [
+ "ruff/e.py",
+ "ruff/e.pyi"
+ ],
+ "ruff/e.py": [],
+ "ruff/e.pyi": []
+ }
- ----- stderr -----
- "###);
+ ----- stderr -----
+ "#);
});
Ok(())
@@ -197,23 +204,96 @@ fn string_detection() -> Result<()> {
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
- assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
- success: true
- exit_code: 0
- ----- stdout -----
- {
- "ruff/__init__.py": [],
- "ruff/a.py": [
- "ruff/b.py"
- ],
- "ruff/b.py": [
- "ruff/c.py"
- ],
- "ruff/c.py": []
- }
+ assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r#"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ {
+ "ruff/__init__.py": [],
+ "ruff/a.py": [
+ "ruff/b.py"
+ ],
+ "ruff/b.py": [],
+ "ruff/c.py": []
+ }
- ----- stderr -----
- "###);
+ ----- stderr -----
+ "#);
+ });
+
+ insta::with_settings!({
+ filters => INSTA_FILTERS.to_vec(),
+ }, {
+ assert_cmd_snapshot!(command().arg("--detect-string-imports").arg("--min-dots").arg("1").current_dir(&root), @r#"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ {
+ "ruff/__init__.py": [],
+ "ruff/a.py": [
+ "ruff/b.py"
+ ],
+ "ruff/b.py": [
+ "ruff/c.py"
+ ],
+ "ruff/c.py": []
+ }
+
+ ----- stderr -----
+ "#);
+ });
+
+ Ok(())
+}
+
+#[test]
+fn string_detection_from_config() -> Result<()> {
+ let tempdir = TempDir::new()?;
+
+ let root = ChildPath::new(tempdir.path());
+
+ // Configure string import detection with a lower min-dots via ruff.toml
+ root.child("ruff.toml").write_str(indoc::indoc! {r#"
+ [analyze]
+ detect-string-imports = true
+ string-imports-min-dots = 1
+ "#})?;
+
+ root.child("ruff").child("__init__.py").write_str("")?;
+ root.child("ruff")
+ .child("a.py")
+ .write_str(indoc::indoc! {r#"
+ import ruff.b
+ "#})?;
+ root.child("ruff")
+ .child("b.py")
+ .write_str(indoc::indoc! {r#"
+ import importlib
+
+ importlib.import_module("ruff.c")
+ "#})?;
+ root.child("ruff").child("c.py").write_str("")?;
+
+ insta::with_settings!({
+ filters => INSTA_FILTERS.to_vec(),
+ }, {
+ assert_cmd_snapshot!(command().current_dir(&root), @r#"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ {
+ "ruff/__init__.py": [],
+ "ruff/a.py": [
+ "ruff/b.py"
+ ],
+ "ruff/b.py": [
+ "ruff/c.py"
+ ],
+ "ruff/c.py": []
+ }
+
+ ----- stderr -----
+ "#);
});
Ok(())
diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs
new file mode 100644
index 0000000000..ebd202b052
--- /dev/null
+++ b/crates/ruff/tests/cli/lint.rs
@@ -0,0 +1,3654 @@
+//! Tests the interaction of the `lint` configuration section
+
+use std::fs;
+use std::process::Command;
+use std::str;
+
+use anyhow::Result;
+
+use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
+
+use crate::CliTest;
+
+const BIN_NAME: &str = "ruff";
+const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
+
+impl CliTest {
+ fn check_command(&self) -> Command {
+ let mut command = self.command();
+ command.args(STDIN_BASE_OPTIONS);
+ command
+ }
+}
+
+#[test]
+fn top_level_options() -> Result<()> {
+ let test = CliTest::new()?;
+ test.write_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+
+[flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(test.check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
+ test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
+ test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
+ Found 3 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ - 'flake8-quotes' -> 'lint.flake8-quotes'
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn lint_options() -> Result<()> {
+ let case = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint]
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ case.check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:5: Q000 [*] Double quotes found but single quotes preferred
+ -:1:5: B005 Using `.strip()` with multi-character strings is misleading
+ -:1:19: Q000 [*] Double quotes found but single quotes preferred
+ Found 3 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Tests that configurations from the top-level and `lint` section are merged together.
+#[test]
+fn mixed_levels() -> Result<()> {
+ let test = CliTest::new()?;
+ test.write_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(test.check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:5: Q000 [*] Double quotes found but single quotes preferred
+ -:1:5: B005 Using `.strip()` with multi-character strings is misleading
+ -:1:19: Q000 [*] Double quotes found but single quotes preferred
+ Found 3 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ ");
+
+ Ok(())
+}
+
+/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific).
+#[test]
+fn precedence() -> Result<()> {
+ let test = CliTest::new()?;
+ test.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+extend-select = ["B", "Q"]
+
+[flake8-quotes]
+inline-quotes = "double"
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(test.check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:5: Q000 [*] Double quotes found but single quotes preferred
+ -:1:5: B005 Using `.strip()` with multi-character strings is misleading
+ -:1:19: Q000 [*] Double quotes found but single quotes preferred
+ Found 3 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'flake8-quotes' -> 'lint.flake8-quotes'
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn exclude() -> Result<()> {
+ let case = CliTest::new()?;
+
+ case.write_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+extend-exclude = ["out"]
+
+[lint]
+exclude = ["test.py", "generated.py"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ case.write_file(
+ "main.py",
+ r#"from test import say_hy
+
+if __name__ == "__main__":
+ say_hy("dear Ruff contributor")
+"#,
+ )?;
+
+ // Excluded file but passed to the CLI directly, should be linted
+ case.write_file(
+ "test.py",
+ r#"def say_hy(name: str):
+ print(f"Hy {name}")"#,
+ )?;
+
+ case.write_file(
+ "generated.py",
+ r#"NUMBERS = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
+]
+OTHER = "OTHER"
+"#,
+ )?;
+
+ case.write_file("out/a.py", r#"a = "a""#)?;
+
+ assert_cmd_snapshot!(
+ case.check_command()
+ .args(["--config", "ruff.toml"])
+ // Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
+ .arg("test.py")
+ // Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options
+ .arg("."), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:3:16: Q000 [*] Double quotes found but single quotes preferred
+ main.py:4:12: Q000 [*] Double quotes found but single quotes preferred
+ test.py:2:15: Q000 [*] Double quotes found but single quotes preferred
+ Found 3 errors.
+ [*] 3 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ ");
+
+ Ok(())
+}
+
+/// Regression test for
+#[test]
+fn deduplicate_directory_and_explicit_file() -> Result<()> {
+ let case = CliTest::new()?;
+
+ case.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+exclude = ["main.py"]
+"#,
+ )?;
+
+ case.write_file("main.py", "import os\n")?;
+
+ assert_cmd_snapshot!(
+ case.check_command()
+ .args(["--config", "ruff.toml"])
+ .arg(".")
+ // Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude
+ .arg("main.py"),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:1:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ "
+ );
+
+ Ok(())
+}
+
+#[test]
+fn exclude_stdin() -> Result<()> {
+ let case = CliTest::with_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+
+[lint]
+exclude = ["generated.py"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ case.check_command()
+ .args(["--config", "ruff.toml"])
+ .args(["--stdin-filename", "generated.py"])
+ .arg("-")
+ .pass_stdin(r#"
+from test import say_hy
+
+if __name__ == "__main__":
+ say_hy("dear Ruff contributor")
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred
+ generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred
+ Found 2 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn line_too_long_width_override() -> Result<()> {
+ let test = CliTest::new()?;
+ test.write_file(
+ "ruff.toml",
+ r#"
+line-length = 80
+select = ["E501"]
+
+[pycodestyle]
+max-line-length = 100
+"#,
+ )?;
+
+ assert_cmd_snapshot!(test.check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"
+# longer than 80, but less than 100
+_ = "---------------------------------------------------------------------------亜亜亜亜亜亜"
+# longer than 100
+_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜"
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:5:91: E501 Line too long (109 > 100)
+ Found 1 error.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'select' -> 'lint.select'
+ - 'pycodestyle' -> 'lint.pycodestyle'
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn per_file_ignores_stdin() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--config", "ruff.toml"])
+ .args(["--stdin-filename", "generated.py"])
+ .args(["--per-file-ignores", "generated.py:Q"])
+ .arg("-")
+ .pass_stdin(r#"
+import os
+
+from test import say_hy
+
+if __name__ == "__main__":
+ say_hy("dear Ruff contributor")
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ generated.py:2:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn extend_per_file_ignores_stdin() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--config", "ruff.toml"])
+ .args(["--stdin-filename", "generated.py"])
+ .args(["--extend-per-file-ignores", "generated.py:Q"])
+ .arg("-")
+ .pass_stdin(r#"
+import os
+
+from test import say_hy
+
+if __name__ == "__main__":
+ say_hy("dear Ruff contributor")
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ generated.py:2:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
+ - 'extend-select' -> 'lint.extend-select'
+ ");
+
+ Ok(())
+}
+
+/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858)
+#[test]
+fn parent_configuration_override() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["ALL"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "subdirectory/ruff.toml",
+ r#"
+[lint]
+ignore = ["D203", "D212"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .current_dir(fixture.root().join("subdirectory"))
+ , @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ warning: No Python files found under the given path(s)
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn nonexistent_config_file() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--config", "foo.toml", "."]), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'foo.toml' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ It looks like you were trying to pass a path to a configuration file.
+ The path `foo.toml` does not point to a configuration file
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn config_override_rejected_if_invalid_toml() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--config", "foo = bar", "."]), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'foo = bar' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ The supplied argument is not valid TOML:
+
+ TOML parse error at line 1, column 7
+ |
+ 1 | foo = bar
+ | ^^^
+ string values must be quoted, expected literal string
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn too_many_config_files() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file("ruff.toml", "")?;
+ fixture.write_file("ruff2.toml", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("--config")
+ .arg("ruff2.toml")
+ .arg("."), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: You cannot specify more than one configuration file on the command line.
+
+ tip: remove either `--config=ruff.toml` or `--config=ruff2.toml`.
+ For more information, try `--help`.
+ ");
+ Ok(())
+}
+
+#[test]
+fn extend_passed_via_config_argument() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--config", "extend = 'foo.toml'", "."]), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'extend = 'foo.toml'' for '--config '
+
+ tip: Cannot include `extend` in a --config flag value
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn nonexistent_extend_file() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+extend = "ruff2.toml"
+"#,
+ )?;
+
+ fixture.write_file(
+ "ruff2.toml",
+ r#"
+extend = "ruff3.toml"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command(), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to load extended configuration `[TMP]/ruff3.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml`)
+ Cause: Failed to read [TMP]/ruff3.toml
+ Cause: No such file or directory (os error 2)
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn circular_extend() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+extend = "ruff2.toml"
+"#,
+ )?;
+ fixture.write_file(
+ "ruff2.toml",
+ r#"
+extend = "ruff3.toml"
+"#,
+ )?;
+ fixture.write_file(
+ "ruff3.toml",
+ r#"
+extend = "ruff.toml"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command(),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Circular configuration detected: `[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml` extends `[TMP]/ruff.toml`
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn parse_error_extends() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+extend = "ruff2.toml"
+"#,
+ )?;
+ fixture.write_file(
+ "ruff2.toml",
+ r#"
+[lint]
+select = [E501]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture.check_command(),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to load extended configuration `[TMP]/ruff2.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml`)
+ Cause: Failed to parse [TMP]/ruff2.toml
+ Cause: TOML parse error at line 3, column 11
+ |
+ 3 | select = [E501]
+ | ^^^^
+ string values must be quoted, expected literal string
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn config_file_and_isolated() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file("ruff.toml", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("--isolated")
+ .arg("."), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: The argument `--config=ruff.toml` cannot be used with `--isolated`
+
+ tip: You cannot specify a configuration file and also specify `--isolated`,
+ as `--isolated` causes ruff to ignore all configuration files.
+ For more information, try `--help`.
+ ");
+ Ok(())
+}
+
+#[test]
+fn config_override_via_cli() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+line-length = 100
+
+[lint]
+select = ["I"]
+
+[lint.isort]
+combine-as-imports = true
+ "#,
+ )?;
+ let test_code = r#"
+from foo import (
+ aaaaaaaaaaaaaaaaaaa,
+ bbbbbbbbbbb as bbbbbbbbbbbbbbbb,
+ cccccccccccccccc,
+ ddddddddddd as ddddddddddddd,
+ eeeeeeeeeeeeeee,
+ ffffffffffff as ffffffffffffff,
+ ggggggggggggg,
+ hhhhhhh as hhhhhhhhhhh,
+ iiiiiiiiiiiiii,
+ jjjjjjjjjjjjj as jjjjjj,
+)
+
+x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
+"#;
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--config", "line-length=90"])
+ .args(["--config", "lint.extend-select=['E501', 'F841']"])
+ .args(["--config", "lint.isort.combine-as-imports = false"])
+ .arg("-")
+ .pass_stdin(test_code), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:2:1: I001 [*] Import block is un-sorted or un-formatted
+ -:15:91: E501 Line too long (97 > 90)
+ Found 2 errors.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+#[test]
+fn valid_toml_but_nonexistent_option_provided_via_config_argument() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args([".", "--config", "extend-select=['F481']"]), // No such code as F481!
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'extend-select=['F481']' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ Could not parse the supplied argument as a `ruff.toml` configuration option:
+
+ Unknown rule selector: `F481`
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn each_toml_option_requires_a_new_flag_1() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ // commas can't be used to delimit different config overrides;
+ // you need a new --config flag for each override
+ .args([".", "--config", "extend-select=['F841'], line-length=90"]),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'extend-select=['F841'], line-length=90' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ The supplied argument is not valid TOML:
+
+ TOML parse error at line 1, column 23
+ |
+ 1 | extend-select=['F841'], line-length=90
+ | ^
+ unexpected key or value, expected newline, `#`
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn each_toml_option_requires_a_new_flag_2() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ // spaces *also* can't be used to delimit different config overrides;
+ // you need a new --config flag for each override
+ .args([".", "--config", "extend-select=['F841'] line-length=90"]),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'extend-select=['F841'] line-length=90' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ The supplied argument is not valid TOML:
+
+ TOML parse error at line 1, column 24
+ |
+ 1 | extend-select=['F841'] line-length=90
+ | ^
+ unexpected key or value, expected newline, `#`
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn value_given_to_table_key_is_not_inline_table_1() {
+ // https://github.com/astral-sh/ruff/issues/13995
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args([".", "--config", r#"lint.flake8-pytest-style="csv""#]),
+ @r#"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'lint.flake8-pytest-style="csv"' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ `lint.flake8-pytest-style` is a table of configuration options.
+ Did you want to override one of the table's subkeys?
+
+ Possible choices:
+
+ - `lint.flake8-pytest-style.fixture-parentheses`
+ - `lint.flake8-pytest-style.parametrize-names-type`
+ - `lint.flake8-pytest-style.parametrize-values-type`
+ - `lint.flake8-pytest-style.parametrize-values-row-type`
+ - `lint.flake8-pytest-style.raises-require-match-for`
+ - `lint.flake8-pytest-style.raises-extend-require-match-for`
+ - `lint.flake8-pytest-style.mark-parentheses`
+ - `lint.flake8-pytest-style.warns-require-match-for`
+ - `lint.flake8-pytest-style.warns-extend-require-match-for`
+
+ For more information, try '--help'.
+ "#);
+}
+
+#[test]
+fn value_given_to_table_key_is_not_inline_table_2() {
+ // https://github.com/astral-sh/ruff/issues/13995
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args([".", "--config", r#"lint=123"#]),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'lint=123' for '--config '
+
+ tip: A `--config` flag must either be a path to a `.toml` configuration file
+ or a TOML ` = ` pair overriding a specific configuration
+ option
+
+ `lint` is a table of configuration options.
+ Did you want to override one of the table's subkeys?
+
+ Possible choices:
+
+ - `lint.allowed-confusables`
+ - `lint.dummy-variable-rgx`
+ - `lint.extend-ignore`
+ - `lint.extend-select`
+ - `lint.extend-fixable`
+ - `lint.external`
+ - `lint.fixable`
+ - `lint.ignore`
+ - `lint.extend-safe-fixes`
+ - `lint.extend-unsafe-fixes`
+ - `lint.ignore-init-module-imports`
+ - `lint.logger-objects`
+ - `lint.select`
+ - `lint.explicit-preview-rules`
+ - `lint.task-tags`
+ - `lint.typing-modules`
+ - `lint.unfixable`
+ - `lint.per-file-ignores`
+ - `lint.extend-per-file-ignores`
+ - `lint.exclude`
+ - `lint.preview`
+ - `lint.typing-extensions`
+ - `lint.future-annotations`
+
+ For more information, try '--help'.
+ ");
+}
+
+#[test]
+fn config_doubly_overridden_via_cli() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+line-length = 100
+
+[lint]
+select=["E501"]
+"#,
+ )?;
+ let test_code = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'";
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ // The --line-length flag takes priority over both the config file
+ // and the `--config="line-length=110"` flag,
+ // despite them both being specified after this flag on the command line:
+ .args(["--line-length", "90"])
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--config", "line-length=110"])
+ .arg("-")
+ .pass_stdin(test_code), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:91: E501 Line too long (97 > 90)
+ Found 1 error.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+#[test]
+fn complex_config_setting_overridden_via_cli() -> Result<()> {
+ let fixture = CliTest::with_file("ruff.toml", "lint.select = ['N801']")?;
+ let test_code = "class violates_n801: pass";
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"])
+ .args(["--stdin-filename", "generated.py"])
+ .arg("-")
+ .pass_stdin(test_code), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+#[test]
+fn deprecated_config_option_overridden_via_cli() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--config", "select=['N801']", "-"])
+ .pass_stdin("class lowercase: ..."),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:7: N801 Class name `lowercase` should use CapWords convention
+ Found 1 error.
+
+ ----- stderr -----
+ warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your `--config` CLI arguments:
+ - 'select' -> 'lint.select'
+ ");
+}
+
+#[test]
+fn extension() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+include = ["*.ipy"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "main.ipy",
+ r#"
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--config", "ruff.toml"])
+ .args(["--extension", "ipy:ipynb"])
+ .arg("."), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.ipy:cell 1:1:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn warn_invalid_noqa_with_no_diagnostics() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--isolated"])
+ .arg("--select")
+ .arg("F401")
+ .arg("-")
+ .pass_stdin(
+ r#"
+# ruff: noqa: AAA101
+print("Hello world!")
+"#
+ )
+ );
+}
+
+#[test]
+fn file_noqa_external() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint]
+external = ["AAA"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"
+# flake8: noqa: AAA101, BBB102
+import os
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:3:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn required_version_exact_mismatch() -> Result<()> {
+ let version = env!("CARGO_PKG_VERSION");
+
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+required-version = "0.1.0"
+"#,
+ )?;
+
+ insta::with_settings!({
+ filters => vec![(version, "[VERSION]")]
+ }, {
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"
+import os
+"#), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
+ ");
+ });
+
+ Ok(())
+}
+
+#[test]
+fn required_version_exact_match() -> Result<()> {
+ let version = env!("CARGO_PKG_VERSION");
+
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ &format!(
+ r#"
+required-version = "{version}"
+"#
+ ),
+ )?;
+
+ insta::with_settings!({
+ filters => vec![(version, "[VERSION]")]
+ }, {
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"
+import os
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:2:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ });
+
+ Ok(())
+}
+
+#[test]
+fn required_version_bound_mismatch() -> Result<()> {
+ let version = env!("CARGO_PKG_VERSION");
+
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ &format!(
+ r#"
+required-version = ">{version}"
+"#
+ ),
+ )?;
+
+ insta::with_settings!({
+ filters => vec![(version, "[VERSION]")]
+ }, {
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"
+import os
+"#), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
+ ");
+ });
+
+ Ok(())
+}
+
+#[test]
+fn required_version_bound_match() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+required-version = ">=0.1.0"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin(r#"
+import os
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:2:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Expand environment variables in `--config` paths provided via the CLI.
+#[test]
+fn config_expand() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["F"]
+ignore = ["F841"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("${NAME}.toml")
+ .env("NAME", "ruff")
+ .arg("-")
+ .pass_stdin(r#"
+import os
+
+def func():
+ x = 1
+"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:2:8: F401 [*] `os` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Per-file selects via ! negation in per-file-ignores
+#[test]
+fn negated_per_file_ignores() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint.per-file-ignores]
+"!selected.py" = ["RUF"]
+"#,
+ )?;
+ fixture.write_file("selected.py", "")?;
+ fixture.write_file("ignored.py", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("--select")
+ .arg("RUF901")
+ , @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+#[test]
+fn negated_per_file_ignores_absolute() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint.per-file-ignores]
+"!src/**.py" = ["RUF"]
+"#,
+ )?;
+ fixture.write_file("src/selected.py", "")?;
+ fixture.write_file("ignored.py", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("--select")
+ .arg("RUF901")
+ , @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+/// patterns are additive, can't use negative patterns to "un-ignore"
+#[test]
+fn negated_per_file_ignores_overlap() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint.per-file-ignores]
+"*.py" = ["RUF"]
+"!foo.py" = ["RUF"]
+"#,
+ )?;
+ fixture.write_file("foo.py", "")?;
+ fixture.write_file("bar.py", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("--select")
+ .arg("RUF901")
+ , @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+#[test]
+fn unused_interaction() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["F"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("--fix")
+ .arg("-")
+ .pass_stdin(r#"
+import os # F401
+
+def function():
+ import os # F811
+ print(os.name)
+"#), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ import os # F401
+
+ def function():
+ print(os.name)
+
+ ----- stderr -----
+ Found 1 error (1 fixed, 0 remaining).
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["RUF015"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+def first_square():
+ return [x * x for x in range(20)][0]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--config", "ruff.toml"])
+ .arg("noqa.py")
+ .args(["--add-noqa"])
+ .arg("-")
+ .pass_stdin(r#"
+
+"#), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Added 1 noqa directive.
+ ");
+
+ let test_code =
+ fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
+
+ insta::assert_snapshot!(test_code, @r"
+ def first_square():
+ return [x * x for x in range(20)][0] # noqa: RUF015
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa_multiple_codes() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["ANN001", "ANN201", "ARG001", "D103"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+def unused(x):
+ pass
+"#,
+ )?;
+
+ 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 -----
+ Added 1 noqa directive.
+ ");
+
+ let test_code =
+ fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
+
+ insta::assert_snapshot!(test_code, @r"
+ def unused(x): # noqa: ANN001, ANN201, D103
+ pass
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa_multiline_diagnostic() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["I"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+import z
+import c
+import a
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--config", "ruff.toml"])
+ .arg("noqa.py")
+ .args(["--add-noqa"])
+ .arg("-")
+ .pass_stdin(r#"
+
+"#), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Added 1 noqa directive.
+ ");
+
+ let test_code =
+ fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
+
+ insta::assert_snapshot!(test_code, @r"
+ import z # noqa: I001
+ import c
+ import a
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa_existing_noqa() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["ANN001", "ANN201", "ARG001", "D103"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+def unused(x): # noqa: ANN001, ARG001, D103
+ pass
+"#,
+ )?;
+
+ 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 -----
+ Added 1 noqa directive.
+ ");
+
+ let test_code =
+ fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
+
+ insta::assert_snapshot!(test_code, @r"
+ def unused(x): # noqa: ANN001, ANN201, ARG001, D103
+ pass
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa_multiline_comment() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["UP031"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+print(
+ """First line
+ second line
+ third line
+ %s"""
+ % name
+)
+"#,
+ )?;
+
+ 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 -----
+ Added 1 noqa directive.
+ ");
+
+ let test_code =
+ fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
+
+ insta::assert_snapshot!(test_code, @r#"
+ print(
+ """First line
+ second line
+ third line
+ %s""" # noqa: UP031
+ % name
+ )
+ "#);
+
+ Ok(())
+}
+
+#[test]
+fn add_noqa_exclude() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+exclude = ["excluded.py"]
+select = ["RUF015"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "noqa.py",
+ r#"
+def first_square():
+ return [x * x for x in range(20)][0]
+"#,
+ )?;
+
+ fixture.write_file(
+ "excluded.py",
+ r#"
+def first_square():
+ return [x * x for x in range(20)][0]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--add-noqa"]), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Added 1 noqa directive.
+ ");
+
+ Ok(())
+}
+
+/// Regression test for
+#[test]
+fn add_noqa_parent() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "noqa.py",
+ r#"
+from foo import ( # noqa: F401
+ bar
+)
+ "#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--add-noqa")
+ .arg("--select=F401")
+ .arg("noqa.py"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Infer `3.11` from `requires-python` in `pyproject.toml`.
+#[test]
+fn requires_python() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+
+[tool.ruff.lint]
+select = ["UP006"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("pyproject.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ let fixture2 = CliTest::with_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.8"
+
+[tool.ruff.lint]
+select = ["UP006"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture2
+ .check_command()
+ .arg("--config")
+ .arg("pyproject.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Infer `3.11` from `requires-python` in `pyproject.toml`.
+#[test]
+fn requires_python_patch() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11.4"
+
+[tool.ruff.lint]
+select = ["UP006"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("pyproject.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Infer `3.11` from `requires-python` in `pyproject.toml`.
+#[test]
+fn requires_python_equals() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = "== 3.11"
+
+[tool.ruff.lint]
+select = ["UP006"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("pyproject.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Infer `3.11` from `requires-python` in `pyproject.toml`.
+#[test]
+fn requires_python_equals_patch() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = "== 3.11.4"
+
+[tool.ruff.lint]
+select = ["UP006"]
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("pyproject.toml")
+ .args(["--stdin-filename", "test.py"])
+ .arg("-")
+ .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<--- no `[tool.ruff]`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_no_tool() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"from typing import Union;foo: Union[int, str] = 1"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .args(["--select", "UP007"])
+ .arg("test.py")
+ .arg("-")
+ );
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<--- no `[tool.ruff]`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_no_tool_preview_enabled() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"from typing import Union;foo: Union[int, str] = 1"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--preview")
+ .arg("--show-settings")
+ .args(["--select", "UP007"])
+ .arg("test.py")
+ .arg("-")
+ );
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<--- no `[tool.ruff]`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_no_tool_target_version_override() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"from typing import Union;foo: Union[int, str] = 1"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .args(["--select", "UP007"])
+ .args(["--target-version", "py310"])
+ .arg("test.py")
+ .arg("-")
+ );
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<--- no `[tool.ruff]`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_no_tool_with_check() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"from typing import Union;foo: Union[int, str] = 1"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .args(["--select","UP007"])
+ .arg(".")
+ , @r###"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:31: UP007 [*] Use `X | Y` for type annotations
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ "###);
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<-- no [tool.ruff]
+/// ├── ruff.toml #<-- no `target-version`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_ruff_toml_no_target_fallback() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"[lint]
+select = ["UP007"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("test.py")
+ .arg("--show-settings"),
+ );
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml #<-- no [tool.ruff]
+/// ├── ruff.toml #<-- no `target-version`
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"[lint]
+select = ["UP007"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("test.py")
+ , @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:2:31: UP007 [*] Use `X | Y` for type annotations
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── foo
+/// │ ├── pyproject.toml #<-- no [tool.ruff], no `requires-python`
+/// │ └── test.py
+/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python`
+/// ```
+#[test]
+fn requires_python_pyproject_toml_above() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/pyproject.toml",
+ r#"[project]
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .args(["--select", "UP007"])
+ .arg("foo/test.py"),
+ );
+
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── foo
+/// │ ├── pyproject.toml #<-- has [tool.ruff], no `requires-python`
+/// │ └── test.py
+/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python`
+/// ```
+#[test]
+fn requires_python_pyproject_toml_above_with_tool() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/pyproject.toml",
+ r#"
+[tool.ruff]
+target-version = "py310"
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .args(["--select", "UP007"])
+ .arg("foo/test.py"),
+ );
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── foo
+/// │ ├── pyproject.toml #<-- no [tool.ruff]
+/// │ └── test.py
+/// └── ruff.toml #<-- no `target-version`
+/// ```
+#[test]
+fn requires_python_ruff_toml_above() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+[lint]
+select = ["UP007"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.11"
+"#,
+ )?;
+
+ fixture.write_file(
+ "foo/test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .arg("foo/test.py"),
+ );
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .arg("test.py")
+ .current_dir(fixture.root().join("foo")),
+ );
+ Ok(())
+}
+
+/// ```
+/// tmp
+/// ├── pyproject.toml <-- requires >=3.10
+/// ├── ruff.toml <--- extends base
+/// ├── shared
+/// │ └── base_config.toml <-- targets 3.11
+/// └── test.py
+/// ```
+#[test]
+fn requires_python_extend_from_shared_config() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "ruff.toml",
+ r#"
+extend = "./shared/base_config.toml"
+[lint]
+select = ["UP007"]
+"#,
+ )?;
+
+ fixture.write_file(
+ "pyproject.toml",
+ r#"[project]
+requires-python = ">= 3.10"
+"#,
+ )?;
+
+ fixture.write_file(
+ "shared/base_config.toml",
+ r#"
+target-version = "py311"
+"#,
+ )?;
+
+ fixture.write_file(
+ "test.py",
+ r#"
+from typing import Union;foo: Union[int, str] = 1
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture
+ .check_command()
+ .arg("--show-settings")
+ .arg("test.py"),
+ );
+ Ok(())
+}
+
+#[test]
+fn checks_notebooks_in_stable() -> anyhow::Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file(
+ "main.ipynb",
+ r#"
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--select")
+ .arg("F401")
+ , @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.ipynb:cell 1:1:8: F401 [*] `random` imported but unused
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+/// Verify that implicit namespace packages are detected even when they are nested.
+///
+/// See:
+#[test]
+fn nested_implicit_namespace_package() -> Result<()> {
+ let fixture = CliTest::new()?;
+
+ fixture.write_file("foo/__init__.py", "")?;
+ fixture.write_file("foo/bar/baz/__init__.py", "")?;
+ fixture.write_file("foo/bar/baz/bop.py", "")?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--select")
+ .arg("INP")
+ , @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ ");
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--select")
+ .arg("INP")
+ .arg("--preview")
+ , @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ foo/bar/baz/__init__.py:1:1: INP001 File `foo/bar/baz/__init__.py` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `foo/bar`.
+ Found 1 error.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint.flake8-import-conventions.aliases]
+"module.name" = "invalid.alias"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin("")
+ , @r#"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to load configuration `[TMP]/ruff.toml`
+ Cause: Failed to parse [TMP]/ruff.toml
+ Cause: TOML parse error at line 3, column 17
+ |
+ 3 | "module.name" = "invalid.alias"
+ | ^^^^^^^^^^^^^^^
+ invalid value: string "invalid.alias", expected a Python identifier
+ "#);
+ Ok(())
+}
+
+#[test]
+fn flake8_import_convention_invalid_aliases_config_extend_alias_name() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint.flake8-import-conventions.extend-aliases]
+"module.name" = "__debug__"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin("")
+ , @r#"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to load configuration `[TMP]/ruff.toml`
+ Cause: Failed to parse [TMP]/ruff.toml
+ Cause: TOML parse error at line 3, column 17
+ |
+ 3 | "module.name" = "__debug__"
+ | ^^^^^^^^^^^
+ invalid value: string "__debug__", expected an assignable Python identifier
+ "#);
+ Ok(())
+}
+
+#[test]
+fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint.flake8-import-conventions.aliases]
+"module..invalid" = "alias"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin("")
+ , @r#"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to load configuration `[TMP]/ruff.toml`
+ Cause: Failed to parse [TMP]/ruff.toml
+ Cause: TOML parse error at line 3, column 1
+ |
+ 3 | "module..invalid" = "alias"
+ | ^^^^^^^^^^^^^^^^^
+ invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods
+ "#);
+ Ok(())
+}
+
+#[test]
+fn flake8_import_convention_nfkc_normalization() -> Result<()> {
+ let fixture = CliTest::with_file(
+ "ruff.toml",
+ r#"
+[lint.flake8-import-conventions.aliases]
+"test.module" = "_﹏𝘥𝘦𝘣𝘶𝘨﹏﹏"
+"#,
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--config")
+ .arg("ruff.toml")
+ .arg("-")
+ .pass_stdin("")
+ , @r###"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Invalid alias for module 'test.module': alias normalizes to '__debug__', which is not allowed.
+ "###);
+ Ok(())
+}
+
+#[test]
+fn flake8_import_convention_unused_aliased_import() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .arg("--config")
+ .arg(r#"lint.isort.required-imports = ["import pandas"]"#)
+ .args(["--select", "I002,ICN001,F401"])
+ .args(["--stdin-filename", "test.py"])
+ .arg("--unsafe-fixes")
+ .arg("--fix")
+ .arg("-")
+ .pass_stdin("1")
+ );
+}
+
+#[test]
+fn flake8_import_convention_unused_aliased_import_no_conflict() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .arg("--config")
+ .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#)
+ .args(["--select", "I002,ICN001,F401"])
+ .args(["--stdin-filename", "test.py"])
+ .arg("--unsafe-fixes")
+ .arg("--fix")
+ .arg("-")
+ .pass_stdin("1")
+ );
+}
+
+// https://github.com/astral-sh/ruff/issues/19842
+#[test]
+fn pyupgrade_up026_respects_isort_required_import_fix() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .arg("--isolated")
+ .arg("check")
+ .arg("-")
+ .args(["--select", "I002,UP026"])
+ .arg("--config")
+ .arg(r#"lint.isort.required-imports=["import mock"]"#)
+ .arg("--fix")
+ .arg("--no-cache")
+ .pass_stdin("1\n"),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ import mock
+ 1
+
+ ----- stderr -----
+ Found 1 error (1 fixed, 0 remaining).
+ "
+ );
+}
+
+// https://github.com/astral-sh/ruff/issues/19842
+#[test]
+fn pyupgrade_up026_respects_isort_required_import_from_fix() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .arg("--isolated")
+ .arg("check")
+ .arg("-")
+ .args(["--select", "I002,UP026"])
+ .arg("--config")
+ .arg(r#"lint.isort.required-imports = ["from mock import mock"]"#)
+ .arg("--fix")
+ .arg("--no-cache")
+ .pass_stdin("from mock import mock\n"),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ from mock import mock
+
+ ----- stderr -----
+ All checks passed!
+ "
+ );
+}
+
+// See: https://github.com/astral-sh/ruff/issues/16177
+#[test]
+fn flake8_pyi_redundant_none_literal() {
+ let snippet = r#"
+from typing import Literal
+
+# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
+# but not both, as if both were autofixed it would result in `None | None`,
+# which leads to a `TypeError` at runtime.
+a: Literal[None,] | Literal[None,]
+b: Literal[None] | Literal[None]
+c: Literal[None] | Literal[None,]
+d: Literal[None,] | Literal[None]
+"#;
+
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--select", "PYI061"])
+ .args(["--stdin-filename", "test.py"])
+ .arg("--preview")
+ .arg("--diff")
+ .arg("-")
+ .pass_stdin(snippet), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ --- test.py
+ +++ test.py
+ @@ -4,7 +4,7 @@
+ # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
+ # but not both, as if both were autofixed it would result in `None | None`,
+ # which leads to a `TypeError` at runtime.
+ -a: Literal[None,] | Literal[None,]
+ -b: Literal[None] | Literal[None]
+ -c: Literal[None] | Literal[None,]
+ -d: Literal[None,] | Literal[None]
+ +a: None | Literal[None,]
+ +b: None | Literal[None]
+ +c: None | Literal[None,]
+ +d: None | Literal[None]
+
+
+ ----- stderr -----
+ Would fix 4 errors.
+ ");
+}
+
+/// Test that private, old-style `TypeVar` generics
+/// 1. Get replaced with PEP 695 type parameters (UP046, UP047)
+/// 2. Get renamed to remove leading underscores (UP049)
+/// 3. Emit a warning that the standalone type variable is now unused (PYI018)
+/// 4. Remove the now-unused `Generic` import
+#[test]
+fn pep695_generic_rename() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--select", "F401,PYI018,UP046,UP047,UP049"])
+ .args(["--stdin-filename", "test.py"])
+ .arg("--unsafe-fixes")
+ .arg("--fix")
+ .arg("--preview")
+ .arg("--target-version=py312")
+ .arg("-")
+ .pass_stdin(
+ r#"
+from typing import Generic, TypeVar
+_T = TypeVar("_T")
+
+class OldStyle(Generic[_T]):
+ var: _T
+
+def func(t: _T) -> _T:
+ x: _T
+ return x
+"#
+ ),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+
+ class OldStyle[T]:
+ var: T
+
+ def func[T](t: T) -> T:
+ x: T
+ return x
+
+ ----- stderr -----
+ Found 7 errors (7 fixed, 0 remaining).
+ "
+ );
+}
+
+/// Test that we do not rename two different type parameters to the same name
+/// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would
+/// introduce invalid syntax)
+#[test]
+fn type_parameter_rename_isolation() {
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--select", "UP049"])
+ .args(["--stdin-filename", "test.py"])
+ .arg("--unsafe-fixes")
+ .arg("--fix")
+ .arg("--preview")
+ .arg("--target-version=py312")
+ .arg("-")
+ .pass_stdin(
+ r#"
+class Foo[_T, __T]:
+ pass
+"#
+ ),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ class Foo[T, __T]:
+ pass
+
+ ----- stderr -----
+ test.py:2:14: UP049 Generic class uses private type parameters
+ Found 2 errors (1 fixed, 1 remaining).
+ "
+ );
+}
+
+/// construct a directory tree with this structure:
+/// .
+/// ├── abc
+/// │ └── __init__.py
+/// ├── collections
+/// │ ├── __init__.py
+/// │ ├── abc
+/// │ │ └── __init__.py
+/// │ └── foobar
+/// │ └── __init__.py
+/// ├── foobar
+/// │ ├── __init__.py
+/// │ ├── abc
+/// │ │ └── __init__.py
+/// │ └── collections
+/// │ ├── __init__.py
+/// │ ├── abc
+/// │ │ └── __init__.py
+/// │ └── foobar
+/// │ └── __init__.py
+/// ├── ruff.toml
+/// └── urlparse
+/// └── __init__.py
+fn create_a005_module_structure(fixture: &CliTest) -> Result<()> {
+ // Create module structure
+ fixture.write_file("abc/__init__.py", "")?;
+ fixture.write_file("collections/__init__.py", "")?;
+ fixture.write_file("collections/abc/__init__.py", "")?;
+ fixture.write_file("collections/foobar/__init__.py", "")?;
+ fixture.write_file("foobar/__init__.py", "")?;
+ fixture.write_file("foobar/abc/__init__.py", "")?;
+ fixture.write_file("foobar/collections/__init__.py", "")?;
+ fixture.write_file("foobar/collections/abc/__init__.py", "")?;
+ fixture.write_file("urlparse/__init__.py", "")?;
+ // also create a ruff.toml to mark the project root
+ fixture.write_file("ruff.toml", "")?;
+
+ Ok(())
+}
+
+/// Test A005 with `strict-checking = true`
+#[test]
+fn a005_module_shadowing_strict() -> Result<()> {
+ let fixture = CliTest::new()?;
+ create_a005_module_structure(&fixture)?;
+
+ assert_cmd_snapshot!(fixture.check_command()
+ .arg("--config")
+ .arg(r#"lint.flake8-builtins.strict-checking = true"#)
+ .args(["--select", "A005"]),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
+ collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
+ foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ Found 6 errors.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Test A005 with `strict-checking = false`
+#[test]
+fn a005_module_shadowing_non_strict() -> Result<()> {
+ let fixture = CliTest::new()?;
+ create_a005_module_structure(&fixture)?;
+
+ assert_cmd_snapshot!(fixture.check_command()
+ .arg("--config")
+ .arg(r#"lint.flake8-builtins.strict-checking = false"#)
+ .args(["--select", "A005"]),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
+ Found 2 errors.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Test A005 with `strict-checking` unset
+///
+/// This should match the non-strict version directly above
+/// Test A005 with `strict-checking` default (should be `false`)
+#[test]
+fn a005_module_shadowing_strict_default() -> Result<()> {
+ let fixture = CliTest::new()?;
+ create_a005_module_structure(&fixture)?;
+
+ assert_cmd_snapshot!(fixture.check_command()
+ .args(["--select", "A005"]),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
+ collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
+ Found 2 errors.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Test that the linter respects per-file-target-version.
+#[test]
+fn per_file_target_version_linter() {
+ // without per-file-target-version, there should be one UP046 error
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--target-version", "py312"])
+ .args(["--select", "UP046"]) // only triggers on 3.12+
+ .args(["--stdin-filename", "test.py"])
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin(r#"
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+class A(Generic[T]):
+ var: T
+"#),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters
+ Found 1 error.
+ No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
+
+ ----- stderr -----
+ "
+ );
+
+ // with per-file-target-version, there should be no errors because the new generic syntax is
+ // unavailable
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--target-version", "py312"])
+ .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
+ .args(["--select", "UP046"]) // only triggers on 3.12+
+ .args(["--stdin-filename", "test.py"])
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin(r#"
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+class A(Generic[T]):
+ var: T
+"#),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ "
+ );
+}
+
+#[test]
+fn walrus_before_py38() {
+ // ok
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--stdin-filename", "test.py"])
+ .arg("--target-version=py38")
+ .arg("-")
+ .pass_stdin(r#"(x := 1)"#),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ "
+ );
+
+ // not ok on 3.7 with preview
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--stdin-filename", "test.py"])
+ .arg("--target-version=py37")
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin(r#"(x := 1)"#),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
+ Found 1 error.
+
+ ----- stderr -----
+ "
+ );
+}
+
+#[test]
+fn match_before_py310() {
+ // ok on 3.10
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--stdin-filename", "test.py"])
+ .arg("--target-version=py310")
+ .arg("-")
+ .pass_stdin(
+ r#"
+match 2:
+ case 1:
+ print("it's one")
+"#
+ ),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ "
+ );
+
+ // ok on 3.9 without preview
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--stdin-filename", "test.py"])
+ .arg("--target-version=py39")
+ .arg("-")
+ .pass_stdin(
+ r#"
+match 2:
+ case 1:
+ print("it's one")
+"#
+ ),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+ Found 1 error.
+
+ ----- stderr -----
+ "
+ );
+
+ // syntax error on 3.9 with preview
+ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--stdin-filename", "test.py"])
+ .arg("--target-version=py39")
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin(
+ r#"
+match 2:
+ case 1:
+ print("it's one")
+"#
+ ),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+ Found 1 error.
+
+ ----- stderr -----
+ "
+ );
+}
+
+/// Regression test for
+#[test]
+fn cache_syntax_errors() -> Result<()> {
+ let fixture = CliTest::with_file("main.py", "match 2:\n case 1: ...")?;
+
+ let mut cmd = fixture.command();
+ // inline STDIN_BASE_OPTIONS to remove --no-cache
+ cmd.args(["check", "--output-format", "concise"])
+ .arg("--target-version=py39")
+ .arg("--preview")
+ .arg("--quiet"); // suppress `debug build without --no-cache` warnings
+
+ assert_cmd_snapshot!(
+ cmd,
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+ ----- stderr -----
+ "
+ );
+
+ // this should *not* be cached, like normal parse errors
+ assert_cmd_snapshot!(
+ cmd,
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+ ----- stderr -----
+ "
+ );
+
+ Ok(())
+}
+
+/// Regression test for with very helpful
+/// reproduction repo here:
+#[test]
+fn cookiecutter_globbing() -> Result<()> {
+ // This is a simplified directory structure from the repo linked above. The essence of the
+ // problem is this `{{cookiecutter.repo_name}}` directory containing a config file with a glob.
+ // The absolute path of the glob contains the glob metacharacters `{{` and `}}` even though the
+ // user's glob does not.
+ let fixture = CliTest::new()?;
+
+ fixture.write_file(
+ "{{cookiecutter.repo_name}}/pyproject.toml",
+ r#"tool.ruff.lint.per-file-ignores = { "tests/*" = ["F811"] }"#,
+ )?;
+
+ // F811 example from the docs to ensure the glob still works
+ fixture.write_file(
+ "{{cookiecutter.repo_name}}/tests/maintest.py",
+ "import foo\nimport bar\nimport foo",
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--select=F811"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ ");
+
+ // after removing the config file with the ignore, F811 applies, so the glob worked above
+ fs::remove_file(
+ fixture
+ .root()
+ .join("{{cookiecutter.repo_name}}/pyproject.toml"),
+ )?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .arg("--select=F811"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ {{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+/// Like the test above but exercises the non-absolute path case in `PerFile::new`
+#[test]
+fn cookiecutter_globbing_no_project_root() -> Result<()> {
+ let fixture = CliTest::new()?;
+
+ // Create the nested directory structure
+ fs::create_dir(fixture.root().join("{{cookiecutter.repo_name}}"))?;
+
+ assert_cmd_snapshot!(fixture
+ .check_command()
+ .current_dir(fixture.root().join("{{cookiecutter.repo_name}}"))
+ .args(["--extend-per-file-ignores", "generated.py:Q"]), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ warning: No Python files found under the given path(s)
+ ");
+
+ Ok(())
+}
+
+/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the
+/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise
+/// disabling all AST-based rules).
+#[test]
+fn semantic_syntax_errors() -> Result<()> {
+ let fixture = CliTest::with_file("main.py", "[(x := 1) for x in foo]")?;
+ let contents = "[(x := 1) for x in foo]";
+
+ let mut cmd = fixture.command();
+ // inline STDIN_BASE_OPTIONS to remove --no-cache
+ cmd.args(["check", "--output-format", "concise"])
+ .arg("--preview")
+ .arg("--quiet"); // suppress `debug build without --no-cache` warnings
+
+ assert_cmd_snapshot!(
+ cmd,
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
+ main.py:1:20: F821 Undefined name `foo`
+
+ ----- stderr -----
+ "
+ );
+
+ // this should *not* be cached, like normal parse errors
+ assert_cmd_snapshot!(
+ cmd,
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
+ main.py:1:20: F821 Undefined name `foo`
+
+ ----- stderr -----
+ "
+ );
+
+ // ensure semantic errors are caught even without AST-based rules selected
+ assert_cmd_snapshot!(
+ fixture.check_command()
+ .args(["--config", "lint.select = []"])
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin(contents),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ -:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
+ Found 1 error.
+
+ ----- stderr -----
+ "
+ );
+
+ Ok(())
+}
+
+/// Regression test for .
+///
+/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would
+/// try to import `Self` from `typing_extensions`
+#[test]
+fn combine_typing_extensions_config() {
+ let contents = "
+from typing import TypeVar
+T = TypeVar('T')
+class Foo:
+ def f(self: T) -> T: ...
+";
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--config", "lint.typing-extensions = false"])
+ .arg("--select=PYI019")
+ .arg("--target-version=py39")
+ .arg("-")
+ .pass_stdin(contents),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ All checks passed!
+
+ ----- stderr -----
+ "
+ );
+}
+
+#[test_case::test_case("concise")]
+#[test_case::test_case("full")]
+#[test_case::test_case("json")]
+#[test_case::test_case("json-lines")]
+#[test_case::test_case("junit")]
+#[test_case::test_case("grouped")]
+#[test_case::test_case("github")]
+#[test_case::test_case("gitlab")]
+#[test_case::test_case("pylint")]
+#[test_case::test_case("rdjson")]
+#[test_case::test_case("azure")]
+#[test_case::test_case("sarif")]
+fn output_format(output_format: &str) -> Result<()> {
+ const CONTENT: &str = "\
+import os # F401
+x = y # F821
+match 42: # invalid-syntax
+ case _: ...
+";
+
+ let fixture = CliTest::with_settings(|_project_dir, mut settings| {
+ // JSON double escapes backslashes
+ settings.add_filter(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#);
+
+ settings
+ })?;
+
+ fixture.write_file("input.py", CONTENT)?;
+
+ let snapshot = format!("output_format_{output_format}");
+
+ assert_cmd_snapshot!(
+ snapshot,
+ fixture.command().args([
+ "check",
+ "--no-cache",
+ "--output-format",
+ output_format,
+ "--select",
+ "F401,F821",
+ "--target-version",
+ "py39",
+ "input.py",
+ ])
+ );
+
+ Ok(())
+}
+
+#[test_case::test_case("concise"; "concise_show_fixes")]
+#[test_case::test_case("full"; "full_show_fixes")]
+#[test_case::test_case("grouped"; "grouped_show_fixes")]
+fn output_format_show_fixes(output_format: &str) -> Result<()> {
+ let fixture = CliTest::with_file("input.py", "import os # F401")?;
+ let snapshot = format!("output_format_show_fixes_{output_format}");
+
+ assert_cmd_snapshot!(
+ snapshot,
+ fixture.command().args([
+ "check",
+ "--no-cache",
+ "--output-format",
+ output_format,
+ "--select",
+ "F401",
+ "--fix",
+ "--show-fixes",
+ "input.py",
+ ])
+ );
+
+ Ok(())
+}
+
+#[test]
+fn up045_nested_optional_flatten_all() {
+ let contents = "\
+from typing import Optional
+nested_optional: Optional[Optional[Optional[str]]] = None
+";
+
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(STDIN_BASE_OPTIONS)
+ .args(["--select", "UP045", "--diff", "--target-version", "py312"])
+ .arg("-")
+ .pass_stdin(contents),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ @@ -1,2 +1,2 @@
+ from typing import Optional
+ -nested_optional: Optional[Optional[Optional[str]]] = None
+ +nested_optional: str | None = None
+
+
+ ----- stderr -----
+ Would fix 1 error.
+ ",
+ );
+}
+
+#[test]
+fn show_fixes_in_full_output_with_preview_enabled() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(["check", "--no-cache", "--output-format", "full"])
+ .args(["--select", "F401"])
+ .arg("--preview")
+ .arg("-")
+ .pass_stdin("import math"),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ F401 [*] `math` imported but unused
+ --> -:1:8
+ |
+ 1 | import math
+ | ^^^^
+ |
+ help: Remove unused import: `math`
+ - import math
+
+ Found 1 error.
+ [*] 1 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ",
+ );
+}
+
+#[test]
+fn rule_panic_mixed_results_concise() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file("normal.py", "import os")?;
+ fixture.write_file("panic.py", "print('hello, world!')")?;
+
+ assert_cmd_snapshot!(
+ fixture.check_command()
+ .args(["--select", "RUF9", "--preview"])
+ .args(["normal.py", "panic.py"]),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+ normal.py:1:1: RUF900 Hey this is a stable test rule.
+ normal.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
+ normal.py:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ normal.py:1:1: RUF903 Hey this is a stable test rule with a display only fix.
+ normal.py:1:1: RUF911 Hey this is a preview test rule.
+ normal.py:1:1: RUF950 Hey this is a test rule that was redirected from another.
+ panic.py: panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.`
+ Found 7 errors.
+ [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
+
+ ----- stderr -----
+ error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
+
+ https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
+
+ ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn rule_panic_mixed_results_full() -> Result<()> {
+ let fixture = CliTest::new()?;
+ fixture.write_file("normal.py", "import os")?;
+ fixture.write_file("panic.py", "print('hello, world!')")?;
+
+ assert_cmd_snapshot!(
+ fixture.command()
+ .args(["check", "--select", "RUF9", "--preview", "--output-format=full", "--no-cache"])
+ .args(["normal.py", "panic.py"]),
+ @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+ RUF900 Hey this is a stable test rule.
+ --> normal.py:1:1
+
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> normal.py:1:1
+ 1 + # fix from stable-test-rule-safe-fix
+ 2 | import os
+
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> normal.py:1:1
+
+ RUF903 Hey this is a stable test rule with a display only fix.
+ --> normal.py:1:1
+
+ RUF911 Hey this is a preview test rule.
+ --> normal.py:1:1
+
+ RUF950 Hey this is a test rule that was redirected from another.
+ --> normal.py:1:1
+
+ panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.`
+ --> panic.py:1:1
+ info: This indicates a bug in Ruff.
+ info: If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bpanic%5D, we'd be very appreciative!
+ info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information
+
+ Found 7 errors.
+ [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
+
+ ----- stderr -----
+ error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
+
+ https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
+
+ ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
+ ");
+
+ Ok(())
+}
+
+/// Test that the same rule fires across all supported extensions, but not on unsupported files
+#[test]
+fn supported_file_extensions() -> Result<()> {
+ let fixture = CliTest::new()?;
+
+ // Create files of various types
+ // text file
+ fixture.write_file("src/thing.txt", "hello world\n")?;
+ // regular python
+ fixture.write_file("src/thing.py", "import os\nprint('hello world')\n")?;
+ // python typestub
+ fixture.write_file("src/thing.pyi", "import os\nclass foo:\n ...\n")?;
+ // windows gui
+ fixture.write_file("src/thing.pyw", "import os\nprint('hello world')\n")?;
+ // cython
+ fixture.write_file(
+ "src/thing.pyx",
+ "import os\ncdef int add(int a, int b):\n return a + b\n",
+ )?;
+ // notebook
+ fixture.write_file(
+ "src/thing.ipynb",
+ r#"
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture.check_command()
+ .args(["--select", "F401"])
+ .arg("src"),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused
+ src/thing.py:1:8: F401 [*] `os` imported but unused
+ src/thing.pyi:1:8: F401 [*] `os` imported but unused
+ Found 3 errors.
+ [*] 3 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
+
+/// Test that the same rule fires across all supported extensions, but not on unsupported files
+#[test]
+fn supported_file_extensions_preview_enabled() -> Result<()> {
+ let fixture = CliTest::new()?;
+
+ // Create files of various types
+ // text file
+ fixture.write_file("src/thing.txt", "hello world\n")?;
+ // regular python
+ fixture.write_file("src/thing.py", "import os\nprint('hello world')\n")?;
+ // python typestub
+ fixture.write_file("src/thing.pyi", "import os\nclass foo:\n ...\n")?;
+ // windows gui
+ fixture.write_file("src/thing.pyw", "import os\nprint('hello world')\n")?;
+ // cython
+ fixture.write_file(
+ "src/thing.pyx",
+ "import os\ncdef int add(int a, int b):\n return a + b\n",
+ )?;
+ // notebook
+ fixture.write_file(
+ "src/thing.ipynb",
+ r#"
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
+"#,
+ )?;
+
+ assert_cmd_snapshot!(
+ fixture.check_command()
+ .args(["--select", "F401", "--preview"])
+ .arg("src"),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused
+ src/thing.py:1:8: F401 [*] `os` imported but unused
+ src/thing.pyi:1:8: F401 [*] `os` imported but unused
+ src/thing.pyw:1:8: F401 [*] `os` imported but unused
+ Found 4 errors.
+ [*] 4 fixable with the `--fix` option.
+
+ ----- stderr -----
+ ");
+ Ok(())
+}
diff --git a/crates/ruff/tests/cli/main.rs b/crates/ruff/tests/cli/main.rs
new file mode 100644
index 0000000000..91f3704e4c
--- /dev/null
+++ b/crates/ruff/tests/cli/main.rs
@@ -0,0 +1,177 @@
+//! Test fixture utilities for ruff CLI tests
+//!
+//! The core concept is borrowed from ty/tests/cli/main.rs and can be extended
+//! with more functionality from there in the future if needed.
+
+#![cfg(not(target_family = "wasm"))]
+
+use anyhow::{Context as _, Result};
+use insta::internals::SettingsBindDropGuard;
+use insta_cmd::get_cargo_bin;
+use std::{
+ fs,
+ path::{Path, PathBuf},
+ process::Command,
+};
+use tempfile::TempDir;
+
+mod lint;
+
+const BIN_NAME: &str = "ruff";
+
+/// Creates a regex filter for replacing temporary directory paths in snapshots
+pub(crate) fn tempdir_filter(path: impl AsRef) -> String {
+ format!(r"{}[\\/]?", regex::escape(path.as_ref()))
+}
+
+/// A test fixture for running ruff CLI tests with temporary directories and files.
+///
+/// This fixture provides:
+/// - Temporary directory management
+/// - File creation utilities
+/// - Proper snapshot filtering for cross-platform compatibility
+/// - Pre-configured ruff command creation
+///
+/// # Example
+///
+/// ```rust,no_run
+/// use crate::common::RuffTestFixture;
+///
+/// let fixture = RuffTestFixture::with_file("ruff.toml", "select = ['E']")?;
+/// let output = fixture.command().args(["check", "."]).output()?;
+/// ```
+pub(crate) struct CliTest {
+ _temp_dir: TempDir,
+ _settings_scope: SettingsBindDropGuard,
+ project_dir: PathBuf,
+}
+
+impl CliTest {
+ /// Creates a new test fixture with an empty temporary directory.
+ ///
+ /// This sets up:
+ /// - A temporary directory that's automatically cleaned up
+ /// - Insta snapshot filters for cross-platform path compatibility
+ /// - Environment isolation for consistent test behavior
+ pub(crate) fn new() -> Result {
+ Self::with_settings(|_, settings| settings)
+ }
+
+ pub(crate) fn with_settings(
+ setup_settings: impl FnOnce(&Path, insta::Settings) -> insta::Settings,
+ ) -> Result {
+ let temp_dir = TempDir::new()?;
+
+ // Canonicalize the tempdir path because macOS uses symlinks for tempdirs
+ // and that doesn't play well with our snapshot filtering.
+ // Simplify with dunce because otherwise we get UNC paths on Windows.
+ let project_dir = dunce::simplified(
+ &temp_dir
+ .path()
+ .canonicalize()
+ .context("Failed to canonicalize project path")?,
+ )
+ .to_path_buf();
+
+ let mut settings = setup_settings(&project_dir, insta::Settings::clone_current());
+
+ settings.add_filter(&tempdir_filter(project_dir.to_str().unwrap()), "[TMP]/");
+ settings.add_filter(r#"\\([\w&&[^nr"]]\w|\s|\.)"#, "/$1");
+ settings.add_filter(r"(Panicked at) [^:]+:\d+:\d+", "$1 ");
+ settings.add_filter(ruff_linter::VERSION, "[VERSION]");
+ settings.add_filter(
+ r#"The system cannot find the file specified."#,
+ "No such file or directory",
+ );
+
+ let settings_scope = settings.bind_to_scope();
+
+ Ok(Self {
+ project_dir,
+ _temp_dir: temp_dir,
+ _settings_scope: settings_scope,
+ })
+ }
+
+ /// Creates a test fixture with a single file.
+ ///
+ /// # Arguments
+ ///
+ /// * `path` - The relative path for the file
+ /// * `content` - The content to write to the file
+ ///
+ /// # Example
+ ///
+ /// ```rust,no_run
+ /// let fixture = RuffTestFixture::with_file("ruff.toml", "select = ['E']")?;
+ /// ```
+ pub(crate) fn with_file(path: impl AsRef, content: &str) -> Result {
+ let fixture = Self::new()?;
+ fixture.write_file(path, content)?;
+ Ok(fixture)
+ }
+
+ /// Ensures that the parent directory of a path exists.
+ fn ensure_parent_directory(path: &Path) -> Result<()> {
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent)
+ .with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
+ }
+ Ok(())
+ }
+
+ /// Writes a file to the test directory.
+ ///
+ /// Parent directories are created automatically if they don't exist.
+ /// Content is dedented to remove common leading whitespace for cleaner test code.
+ ///
+ /// # Arguments
+ ///
+ /// * `path` - The relative path for the file
+ /// * `content` - The content to write to the file
+ pub(crate) fn write_file(&self, path: impl AsRef, content: &str) -> Result<()> {
+ let path = path.as_ref();
+ let file_path = self.project_dir.join(path);
+
+ Self::ensure_parent_directory(&file_path)?;
+
+ let content = ruff_python_trivia::textwrap::dedent(content);
+ fs::write(&file_path, content.as_ref())
+ .with_context(|| format!("Failed to write file `{}`", file_path.display()))?;
+
+ Ok(())
+ }
+
+ /// Returns the path to the test directory root.
+ pub(crate) fn root(&self) -> &Path {
+ &self.project_dir
+ }
+
+ /// Creates a pre-configured ruff command for testing.
+ ///
+ /// The command is set up with:
+ /// - The correct ruff binary path
+ /// - Working directory set to the test directory
+ /// - Clean environment variables for consistent behavior
+ ///
+ /// You can chain additional arguments and options as needed.
+ ///
+ /// # Example
+ ///
+ /// ```rust,no_run
+ /// let output = fixture
+ /// .command()
+ /// .args(["check", "--select", "E"])
+ /// .arg(".")
+ /// .output()?;
+ /// ```
+ pub(crate) fn command(&self) -> Command {
+ let mut command = Command::new(get_cargo_bin(BIN_NAME));
+ command.current_dir(&self.project_dir);
+
+ // Unset all environment variables because they can affect test behavior.
+ command.env_clear();
+
+ command
+ }
+}
diff --git a/crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import.snap b/crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import.snap
similarity index 100%
rename from crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import.snap
rename to crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import.snap
diff --git a/crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import_no_conflict.snap b/crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import_no_conflict.snap
similarity index 100%
rename from crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import_no_conflict.snap
rename to crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import_no_conflict.snap
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_azure.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_azure.snap
new file mode 100644
index 0000000000..933cc043b9
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_azure.snap
@@ -0,0 +1,23 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - azure
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
+##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
+##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;code=invalid-syntax;]Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_concise.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_concise.snap
new file mode 100644
index 0000000000..56bb215b25
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_concise.snap
@@ -0,0 +1,25 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:1:8: F401 [*] `os` imported but unused
+input.py:2:5: F821 Undefined name `y`
+input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+Found 3 errors.
+[*] 1 fixable with the `--fix` option.
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap
new file mode 100644
index 0000000000..d47237df55
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap
@@ -0,0 +1,52 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - full
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+F401 [*] `os` imported but unused
+ --> input.py:1:8
+ |
+1 | import os # F401
+ | ^^
+2 | x = y # F821
+3 | match 42: # invalid-syntax
+ |
+help: Remove unused import: `os`
+
+F821 Undefined name `y`
+ --> input.py:2:5
+ |
+1 | import os # F401
+2 | x = y # F821
+ | ^
+3 | match 42: # invalid-syntax
+4 | case _: ...
+ |
+
+invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+ --> input.py:3:1
+ |
+1 | import os # F401
+2 | x = y # F821
+3 | match 42: # invalid-syntax
+ | ^^^^^
+4 | case _: ...
+ |
+
+Found 3 errors.
+[*] 1 fixable with the `--fix` option.
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap
new file mode 100644
index 0000000000..0e7ff40baa
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap
@@ -0,0 +1,23 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - github
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
+::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
+::error title=Ruff (invalid-syntax),file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_gitlab.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_gitlab.snap
new file mode 100644
index 0000000000..f16851bb3a
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_gitlab.snap
@@ -0,0 +1,78 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - gitlab
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+[
+ {
+ "check_name": "F401",
+ "description": "F401: `os` imported but unused",
+ "severity": "major",
+ "fingerprint": "4dbad37161e65c72",
+ "location": {
+ "path": "input.py",
+ "positions": {
+ "begin": {
+ "line": 1,
+ "column": 8
+ },
+ "end": {
+ "line": 1,
+ "column": 10
+ }
+ }
+ }
+ },
+ {
+ "check_name": "F821",
+ "description": "F821: Undefined name `y`",
+ "severity": "major",
+ "fingerprint": "7af59862a085230",
+ "location": {
+ "path": "input.py",
+ "positions": {
+ "begin": {
+ "line": 2,
+ "column": 5
+ },
+ "end": {
+ "line": 2,
+ "column": 6
+ }
+ }
+ }
+ },
+ {
+ "check_name": "invalid-syntax",
+ "description": "invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
+ "severity": "major",
+ "fingerprint": "e558cec859bb66e8",
+ "location": {
+ "path": "input.py",
+ "positions": {
+ "begin": {
+ "line": 3,
+ "column": 1
+ },
+ "end": {
+ "line": 3,
+ "column": 6
+ }
+ }
+ }
+ }
+]
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_grouped.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_grouped.snap
new file mode 100644
index 0000000000..acf3decff8
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_grouped.snap
@@ -0,0 +1,27 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - grouped
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:
+ 1:8 F401 [*] `os` imported but unused
+ 2:5 F821 Undefined name `y`
+ 3:1 invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+Found 3 errors.
+[*] 1 fixable with the `--fix` option.
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json-lines.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json-lines.snap
new file mode 100644
index 0000000000..868c92c728
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json-lines.snap
@@ -0,0 +1,23 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - json-lines
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
+{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
+{"cell":null,"code":"invalid-syntax","end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json.snap
new file mode 100644
index 0000000000..4b9695a8f4
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json.snap
@@ -0,0 +1,88 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - json
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+[
+ {
+ "cell": null,
+ "code": "F401",
+ "end_location": {
+ "column": 10,
+ "row": 1
+ },
+ "filename": "[TMP]/input.py",
+ "fix": {
+ "applicability": "safe",
+ "edits": [
+ {
+ "content": "",
+ "end_location": {
+ "column": 1,
+ "row": 2
+ },
+ "location": {
+ "column": 1,
+ "row": 1
+ }
+ }
+ ],
+ "message": "Remove unused import: `os`"
+ },
+ "location": {
+ "column": 8,
+ "row": 1
+ },
+ "message": "`os` imported but unused",
+ "noqa_row": 1,
+ "url": "https://docs.astral.sh/ruff/rules/unused-import"
+ },
+ {
+ "cell": null,
+ "code": "F821",
+ "end_location": {
+ "column": 6,
+ "row": 2
+ },
+ "filename": "[TMP]/input.py",
+ "fix": null,
+ "location": {
+ "column": 5,
+ "row": 2
+ },
+ "message": "Undefined name `y`",
+ "noqa_row": 2,
+ "url": "https://docs.astral.sh/ruff/rules/undefined-name"
+ },
+ {
+ "cell": null,
+ "code": "invalid-syntax",
+ "end_location": {
+ "column": 6,
+ "row": 3
+ },
+ "filename": "[TMP]/input.py",
+ "fix": null,
+ "location": {
+ "column": 1,
+ "row": 3
+ },
+ "message": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
+ "noqa_row": null,
+ "url": null
+ }
+]
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap
new file mode 100644
index 0000000000..384671431f
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap
@@ -0,0 +1,34 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - junit
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+
+
+
+
+ line 1, col 8, `os` imported but unused
+
+
+ line 2, col 5, Undefined name `y`
+
+
+ line 3, col 1, Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+
+
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_pylint.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_pylint.snap
new file mode 100644
index 0000000000..bcedcda242
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_pylint.snap
@@ -0,0 +1,23 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - pylint
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:1: [F401] `os` imported but unused
+input.py:2: [F821] Undefined name `y`
+input.py:3: [invalid-syntax] Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_rdjson.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_rdjson.snap
new file mode 100644
index 0000000000..6ee239e2b4
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_rdjson.snap
@@ -0,0 +1,102 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - rdjson
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+{
+ "diagnostics": [
+ {
+ "code": {
+ "url": "https://docs.astral.sh/ruff/rules/unused-import",
+ "value": "F401"
+ },
+ "location": {
+ "path": "[TMP]/input.py",
+ "range": {
+ "end": {
+ "column": 10,
+ "line": 1
+ },
+ "start": {
+ "column": 8,
+ "line": 1
+ }
+ }
+ },
+ "message": "`os` imported but unused",
+ "suggestions": [
+ {
+ "range": {
+ "end": {
+ "column": 1,
+ "line": 2
+ },
+ "start": {
+ "column": 1,
+ "line": 1
+ }
+ },
+ "text": ""
+ }
+ ]
+ },
+ {
+ "code": {
+ "url": "https://docs.astral.sh/ruff/rules/undefined-name",
+ "value": "F821"
+ },
+ "location": {
+ "path": "[TMP]/input.py",
+ "range": {
+ "end": {
+ "column": 6,
+ "line": 2
+ },
+ "start": {
+ "column": 5,
+ "line": 2
+ }
+ }
+ },
+ "message": "Undefined name `y`"
+ },
+ {
+ "code": {
+ "value": "invalid-syntax"
+ },
+ "location": {
+ "path": "[TMP]/input.py",
+ "range": {
+ "end": {
+ "column": 6,
+ "line": 3
+ },
+ "start": {
+ "column": 1,
+ "line": 3
+ }
+ }
+ },
+ "message": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
+ }
+ ],
+ "severity": "WARNING",
+ "source": {
+ "name": "ruff",
+ "url": "https://docs.astral.sh/ruff"
+ }
+}
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap
new file mode 100644
index 0000000000..2d60b432fb
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap
@@ -0,0 +1,167 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - sarif
+ - "--select"
+ - "F401,F821"
+ - "--target-version"
+ - py39
+ - input.py
+snapshot_kind: text
+---
+success: false
+exit_code: 1
+----- stdout -----
+{
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+ "runs": [
+ {
+ "results": [
+ {
+ "fixes": [
+ {
+ "artifactChanges": [
+ {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "replacements": [
+ {
+ "deletedRegion": {
+ "endColumn": 1,
+ "endLine": 2,
+ "startColumn": 1,
+ "startLine": 1
+ }
+ }
+ ]
+ }
+ ],
+ "description": {
+ "text": "Remove unused import: `os`"
+ }
+ }
+ ],
+ "level": "error",
+ "locations": [
+ {
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "region": {
+ "endColumn": 10,
+ "endLine": 1,
+ "startColumn": 8,
+ "startLine": 1
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "`os` imported but unused"
+ },
+ "ruleId": "F401"
+ },
+ {
+ "level": "error",
+ "locations": [
+ {
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "region": {
+ "endColumn": 6,
+ "endLine": 2,
+ "startColumn": 5,
+ "startLine": 2
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Undefined name `y`"
+ },
+ "ruleId": "F821"
+ },
+ {
+ "level": "error",
+ "locations": [
+ {
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "region": {
+ "endColumn": 6,
+ "endLine": 3,
+ "startColumn": 1,
+ "startLine": 3
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
+ },
+ "ruleId": "invalid-syntax"
+ }
+ ],
+ "tool": {
+ "driver": {
+ "informationUri": "https://github.com/astral-sh/ruff",
+ "name": "ruff",
+ "rules": [
+ {
+ "fullDescription": {
+ "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n"
+ },
+ "help": {
+ "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
+ },
+ "helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
+ "id": "F401",
+ "properties": {
+ "id": "F401",
+ "kind": "Pyflakes",
+ "name": "unused-import",
+ "problem.severity": "error"
+ },
+ "shortDescription": {
+ "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
+ },
+ "help": {
+ "text": "Undefined name `{name}`. {tip}"
+ },
+ "helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
+ "id": "F821",
+ "properties": {
+ "id": "F821",
+ "kind": "Pyflakes",
+ "name": "undefined-name",
+ "problem.severity": "error"
+ },
+ "shortDescription": {
+ "text": "Undefined name `{name}`. {tip}"
+ }
+ }
+ ],
+ "version": "[VERSION]"
+ }
+ }
+ }
+ ],
+ "version": "2.1.0"
+}
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_concise.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_concise.snap
new file mode 100644
index 0000000000..e72b277305
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_concise.snap
@@ -0,0 +1,26 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--select"
+ - F401
+ - "--fix"
+ - "--show-fixes"
+ - input.py
+---
+success: true
+exit_code: 0
+----- stdout -----
+
+Fixed 1 error:
+- input.py:
+ 1 × F401 (unused-import)
+
+Found 1 error (1 fixed, 0 remaining).
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_full.snap
new file mode 100644
index 0000000000..96bbccb76e
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_full.snap
@@ -0,0 +1,26 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - full
+ - "--select"
+ - F401
+ - "--fix"
+ - "--show-fixes"
+ - input.py
+---
+success: true
+exit_code: 0
+----- stdout -----
+
+Fixed 1 error:
+- input.py:
+ 1 × F401 (unused-import)
+
+Found 1 error (1 fixed, 0 remaining).
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap
new file mode 100644
index 0000000000..ce0e16ff12
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap
@@ -0,0 +1,26 @@
+---
+source: crates/ruff/tests/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - grouped
+ - "--select"
+ - F401
+ - "--fix"
+ - "--show-fixes"
+ - input.py
+---
+success: true
+exit_code: 0
+----- stdout -----
+
+Fixed 1 error:
+- input.py:
+ 1 × F401 (unused-import)
+
+Found 1 error (1 fixed, 0 remaining).
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap
new file mode 100644
index 0000000000..62bde10fe3
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap
@@ -0,0 +1,288 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - test.py
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/test.py"
+Settings path: "[TMP]/ruff.toml"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.10
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.10
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.10
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap
new file mode 100644
index 0000000000..a7b4b2c978
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap
@@ -0,0 +1,290 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - "--select"
+ - UP007
+ - test.py
+ - "-"
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/test.py"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.11
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.11
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.11
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap
new file mode 100644
index 0000000000..4e33c123a4
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap
@@ -0,0 +1,292 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--preview"
+ - "--show-settings"
+ - "--select"
+ - UP007
+ - test.py
+ - "-"
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/test.py"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.pyw",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.11
+linter.per_file_target_version = {}
+linter.preview = enabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.11
+formatter.per_file_target_version = {}
+formatter.preview = enabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = enabled
+analyze.target_version = 3.11
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap
new file mode 100644
index 0000000000..929943558e
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap
@@ -0,0 +1,292 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - "--select"
+ - UP007
+ - "--target-version"
+ - py310
+ - test.py
+ - "-"
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/test.py"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.10
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.10
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.10
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap
new file mode 100644
index 0000000000..291cb62d6e
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap
@@ -0,0 +1,289 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - "--select"
+ - UP007
+ - foo/test.py
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/foo/test.py"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.11
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.11
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.11
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap
new file mode 100644
index 0000000000..d9f9402895
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap
@@ -0,0 +1,289 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - "--select"
+ - UP007
+ - foo/test.py
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/foo/test.py"
+
+# General Settings
+cache_dir = "[TMP]/foo/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/foo"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/foo"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.10
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/foo",
+ "[TMP]/foo/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.10
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.10
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap
new file mode 100644
index 0000000000..da3eb66afc
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap
@@ -0,0 +1,287 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - test.py
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/foo/test.py"
+Settings path: "[TMP]/ruff.toml"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = none
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.10
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.10
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap
new file mode 100644
index 0000000000..e9ca7bd400
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap
@@ -0,0 +1,287 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--show-settings"
+ - foo/test.py
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/foo/test.py"
+Settings path: "[TMP]/ruff.toml"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = none
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.10
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.10
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap
new file mode 100644
index 0000000000..a3f9343314
--- /dev/null
+++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap
@@ -0,0 +1,288 @@
+---
+source: crates/ruff/tests/cli/lint.rs
+info:
+ program: ruff
+ args:
+ - check
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - test.py
+ - "--show-settings"
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+Resolved settings for: "[TMP]/test.py"
+Settings path: "[TMP]/ruff.toml"
+
+# General Settings
+cache_dir = "[TMP]/.ruff_cache"
+fix = false
+fix_only = false
+output_format = concise
+show_fixes = false
+unsafe_fixes = hint
+
+# File Resolver Settings
+file_resolver.exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+file_resolver.extend_exclude = []
+file_resolver.force_exclude = false
+file_resolver.include = [
+ "*.py",
+ "*.pyi",
+ "*.ipynb",
+ "**/pyproject.toml",
+]
+file_resolver.extend_include = []
+file_resolver.respect_gitignore = true
+file_resolver.project_root = "[TMP]/"
+
+# Linter Settings
+linter.exclude = []
+linter.project_root = "[TMP]/"
+linter.rules.enabled = [
+ non-pep604-annotation-union (UP007),
+]
+linter.rules.should_fix = [
+ non-pep604-annotation-union (UP007),
+]
+linter.per_file_ignores = {}
+linter.safety_table.forced_safe = []
+linter.safety_table.forced_unsafe = []
+linter.unresolved_target_version = 3.11
+linter.per_file_target_version = {}
+linter.preview = disabled
+linter.explicit_preview_rules = false
+linter.extension = ExtensionMapping({})
+linter.allowed_confusables = []
+linter.builtins = []
+linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
+linter.external = []
+linter.ignore_init_module_imports = true
+linter.logger_objects = []
+linter.namespace_packages = []
+linter.src = [
+ "[TMP]/",
+ "[TMP]/src",
+]
+linter.tab_size = 4
+linter.line_length = 88
+linter.task_tags = [
+ TODO,
+ FIXME,
+ XXX,
+]
+linter.typing_modules = []
+linter.typing_extensions = true
+
+# Linter Plugins
+linter.flake8_annotations.mypy_init_return = false
+linter.flake8_annotations.suppress_dummy_args = false
+linter.flake8_annotations.suppress_none_returning = false
+linter.flake8_annotations.allow_star_arg_any = false
+linter.flake8_annotations.ignore_fully_untyped = false
+linter.flake8_bandit.hardcoded_tmp_directory = [
+ /tmp,
+ /var/tmp,
+ /dev/shm,
+]
+linter.flake8_bandit.check_typed_exception = false
+linter.flake8_bandit.extend_markup_names = []
+linter.flake8_bandit.allowed_markup_calls = []
+linter.flake8_bugbear.extend_immutable_calls = []
+linter.flake8_builtins.allowed_modules = []
+linter.flake8_builtins.ignorelist = []
+linter.flake8_builtins.strict_checking = false
+linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
+linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
+linter.flake8_copyright.author = none
+linter.flake8_copyright.min_file_size = 0
+linter.flake8_errmsg.max_string_length = 0
+linter.flake8_gettext.functions_names = [
+ _,
+ gettext,
+ ngettext,
+]
+linter.flake8_implicit_str_concat.allow_multiline = true
+linter.flake8_import_conventions.aliases = {
+ altair = alt,
+ holoviews = hv,
+ matplotlib = mpl,
+ matplotlib.pyplot = plt,
+ networkx = nx,
+ numpy = np,
+ numpy.typing = npt,
+ pandas = pd,
+ panel = pn,
+ plotly.express = px,
+ polars = pl,
+ pyarrow = pa,
+ seaborn = sns,
+ tensorflow = tf,
+ tkinter = tk,
+ xml.etree.ElementTree = ET,
+}
+linter.flake8_import_conventions.banned_aliases = {}
+linter.flake8_import_conventions.banned_from = []
+linter.flake8_pytest_style.fixture_parentheses = false
+linter.flake8_pytest_style.parametrize_names_type = tuple
+linter.flake8_pytest_style.parametrize_values_type = list
+linter.flake8_pytest_style.parametrize_values_row_type = tuple
+linter.flake8_pytest_style.raises_require_match_for = [
+ BaseException,
+ Exception,
+ ValueError,
+ OSError,
+ IOError,
+ EnvironmentError,
+ socket.error,
+]
+linter.flake8_pytest_style.raises_extend_require_match_for = []
+linter.flake8_pytest_style.mark_parentheses = false
+linter.flake8_quotes.inline_quotes = double
+linter.flake8_quotes.multiline_quotes = double
+linter.flake8_quotes.docstring_quotes = double
+linter.flake8_quotes.avoid_escape = true
+linter.flake8_self.ignore_names = [
+ _make,
+ _asdict,
+ _replace,
+ _fields,
+ _field_defaults,
+ _name_,
+ _value_,
+]
+linter.flake8_tidy_imports.ban_relative_imports = "parents"
+linter.flake8_tidy_imports.banned_api = {}
+linter.flake8_tidy_imports.banned_module_level_imports = []
+linter.flake8_type_checking.strict = false
+linter.flake8_type_checking.exempt_modules = [
+ typing,
+ typing_extensions,
+]
+linter.flake8_type_checking.runtime_required_base_classes = []
+linter.flake8_type_checking.runtime_required_decorators = []
+linter.flake8_type_checking.quote_annotations = false
+linter.flake8_unused_arguments.ignore_variadic_names = false
+linter.isort.required_imports = []
+linter.isort.combine_as_imports = false
+linter.isort.force_single_line = false
+linter.isort.force_sort_within_sections = false
+linter.isort.detect_same_package = true
+linter.isort.case_sensitive = false
+linter.isort.force_wrap_aliases = false
+linter.isort.force_to_top = []
+linter.isort.known_modules = {}
+linter.isort.order_by_type = true
+linter.isort.relative_imports_order = furthest_to_closest
+linter.isort.single_line_exclusions = []
+linter.isort.split_on_trailing_comma = true
+linter.isort.classes = []
+linter.isort.constants = []
+linter.isort.variables = []
+linter.isort.no_lines_before = []
+linter.isort.lines_after_imports = -1
+linter.isort.lines_between_types = 0
+linter.isort.forced_separate = []
+linter.isort.section_order = [
+ known { type = future },
+ known { type = standard_library },
+ known { type = third_party },
+ known { type = first_party },
+ known { type = local_folder },
+]
+linter.isort.default_section = known { type = third_party }
+linter.isort.no_sections = false
+linter.isort.from_first = false
+linter.isort.length_sort = false
+linter.isort.length_sort_straight = false
+linter.mccabe.max_complexity = 10
+linter.pep8_naming.ignore_names = [
+ setUp,
+ tearDown,
+ setUpClass,
+ tearDownClass,
+ setUpModule,
+ tearDownModule,
+ asyncSetUp,
+ asyncTearDown,
+ setUpTestData,
+ failureException,
+ longMessage,
+ maxDiff,
+]
+linter.pep8_naming.classmethod_decorators = []
+linter.pep8_naming.staticmethod_decorators = []
+linter.pycodestyle.max_line_length = 88
+linter.pycodestyle.max_doc_length = none
+linter.pycodestyle.ignore_overlong_task_comments = false
+linter.pyflakes.extend_generics = []
+linter.pyflakes.allowed_unused_imports = []
+linter.pylint.allow_magic_value_types = [
+ str,
+ bytes,
+]
+linter.pylint.allow_dunder_method_names = []
+linter.pylint.max_args = 5
+linter.pylint.max_positional_args = 5
+linter.pylint.max_returns = 6
+linter.pylint.max_bool_expr = 5
+linter.pylint.max_branches = 12
+linter.pylint.max_statements = 50
+linter.pylint.max_public_methods = 20
+linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
+linter.pyupgrade.keep_runtime_typing = false
+linter.ruff.parenthesize_tuple_in_subscript = false
+
+# Formatter Settings
+formatter.exclude = []
+formatter.unresolved_target_version = 3.11
+formatter.per_file_target_version = {}
+formatter.preview = disabled
+formatter.line_width = 88
+formatter.line_ending = auto
+formatter.indent_style = space
+formatter.indent_width = 4
+formatter.quote_style = double
+formatter.magic_trailing_comma = respect
+formatter.docstring_code_format = disabled
+formatter.docstring_code_line_width = dynamic
+
+# Analyze Settings
+analyze.exclude = []
+analyze.preview = disabled
+analyze.target_version = 3.11
+analyze.string_imports = disabled
+analyze.extension = ExtensionMapping({})
+analyze.include_dependencies = {}
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/lint__warn_invalid_noqa_with_no_diagnostics.snap b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap
similarity index 100%
rename from crates/ruff/tests/snapshots/lint__warn_invalid_noqa_with_no_diagnostics.snap
rename to crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap
diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs
index 31991f9b28..6205fa98e8 100644
--- a/crates/ruff/tests/format.rs
+++ b/crates/ruff/tests/format.rs
@@ -12,8 +12,8 @@ use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
-fn tempdir_filter(tempdir: &TempDir) -> String {
- format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
+fn tempdir_filter(path: impl AsRef) -> String {
+ format!(r"{}\\?/?", escape(path.as_ref().to_str().unwrap()))
}
#[test]
@@ -120,7 +120,7 @@ fn nonexistent_config_file() {
#[test]
fn config_override_rejected_if_invalid_toml() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(["format", "--config", "foo = bar", "."]), @r#"
+ .args(["format", "--config", "foo = bar", "."]), @r"
success: false
exit_code: 2
----- stdout -----
@@ -137,12 +137,11 @@ fn config_override_rejected_if_invalid_toml() {
TOML parse error at line 1, column 7
|
1 | foo = bar
- | ^
- invalid string
- expected `"`, `'`
+ | ^^^
+ string values must be quoted, expected literal string
For more information, try '--help'.
- "#);
+ ");
}
#[test]
@@ -501,6 +500,35 @@ OTHER = "OTHER"
Ok(())
}
+/// Regression test for
+#[test]
+fn deduplicate_directory_and_explicit_file() -> Result<()> {
+ let tempdir = TempDir::new()?;
+ let root = tempdir.path();
+
+ let main = root.join("main.py");
+ fs::write(&main, "x = 1\n")?;
+
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .current_dir(root)
+ .args(["format", "--no-cache", "--check"])
+ .arg(".")
+ .arg("main.py"),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ Would reformat: main.py
+ 1 file would be reformatted
+
+ ----- stderr -----
+ "
+ );
+
+ Ok(())
+}
+
#[test]
fn syntax_error() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -581,6 +609,112 @@ if __name__ == "__main__":
Ok(())
}
+#[test_case::test_case("concise")]
+#[test_case::test_case("full")]
+#[test_case::test_case("json")]
+#[test_case::test_case("json-lines")]
+#[test_case::test_case("junit")]
+#[test_case::test_case("grouped")]
+#[test_case::test_case("github")]
+#[test_case::test_case("gitlab")]
+#[test_case::test_case("pylint")]
+#[test_case::test_case("rdjson")]
+#[test_case::test_case("azure")]
+#[test_case::test_case("sarif")]
+fn output_format(output_format: &str) -> Result<()> {
+ const CONTENT: &str = r#"
+from test import say_hy
+
+if __name__ == "__main__":
+ say_hy("dear Ruff contributor")
+"#;
+
+ let tempdir = TempDir::new()?;
+ let input = tempdir.path().join("input.py");
+ fs::write(&input, CONTENT)?;
+
+ let snapshot = format!("output_format_{output_format}");
+
+ let project_dir = dunce::canonicalize(tempdir.path())?;
+
+ insta::with_settings!({
+ filters => vec![
+ (tempdir_filter(&project_dir).as_str(), "[TMP]/"),
+ (tempdir_filter(&tempdir).as_str(), "[TMP]/"),
+ (r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
+ (ruff_linter::VERSION, "[VERSION]"),
+ ]
+ }, {
+ assert_cmd_snapshot!(
+ snapshot,
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args([
+ "format",
+ "--no-cache",
+ "--output-format",
+ output_format,
+ "--preview",
+ "--check",
+ "input.py",
+ ])
+ .current_dir(&tempdir),
+ );
+ });
+
+ Ok(())
+}
+
+#[test]
+fn output_format_notebook() {
+ let args = ["format", "--no-cache", "--isolated", "--preview", "--check"];
+ let fixtures = Path::new("resources").join("test").join("fixtures");
+ let path = fixtures.join("unformatted.ipynb");
+ insta::with_settings!({filters => vec![
+ // Replace windows paths
+ (r"\\", "/"),
+ ]}, {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME)).args(args).arg(path),
+ @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ unformatted: File would be reformatted
+ --> resources/test/fixtures/unformatted.ipynb:cell 1:1:1
+ ::: cell 1
+ 1 | import numpy
+ - maths = (numpy.arange(100)**2).sum()
+ - stats= numpy.asarray([1,2,3,4]).median()
+ 2 +
+ 3 + maths = (numpy.arange(100) ** 2).sum()
+ 4 + stats = numpy.asarray([1, 2, 3, 4]).median()
+ ::: cell 3
+ 1 | # A cell with IPython escape command
+ 2 | def some_function(foo, bar):
+ 3 | pass
+ 4 +
+ 5 +
+ 6 | %matplotlib inline
+ ::: cell 4
+ 1 | foo = %pwd
+ - def some_function(foo,bar,):
+ 2 +
+ 3 +
+ 4 + def some_function(
+ 5 + foo,
+ 6 + bar,
+ 7 + ):
+ 8 | # Another cell with IPython escape command
+ 9 | foo = %pwd
+ 10 | print(foo)
+
+ 1 file would be reformatted
+
+ ----- stderr -----
+ ");
+ });
+}
+
#[test]
fn exit_non_zero_on_format() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -2327,3 +2461,21 @@ fn cookiecutter_globbing() -> Result<()> {
Ok(())
}
+
+#[test]
+fn stable_output_format_warning() {
+ assert_cmd_snapshot!(
+ Command::new(get_cargo_bin(BIN_NAME))
+ .args(["format", "--output-format=full", "-"])
+ .pass_stdin("1"),
+ @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ 1
+
+ ----- stderr -----
+ warning: The --output-format flag for the formatter is unstable and requires preview mode to use.
+ ",
+ );
+}
diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs
index 3e8f5497ed..edca13cd72 100644
--- a/crates/ruff/tests/integration_test.rs
+++ b/crates/ruff/tests/integration_test.rs
@@ -115,12 +115,13 @@ fn stdin_error() {
success: false
exit_code: 1
----- stdout -----
- -:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> -:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -139,12 +140,13 @@ fn stdin_filename() {
success: false
exit_code: 1
----- stdout -----
- F401.py:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> F401.py:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -174,19 +176,21 @@ import bar # unused import
success: false
exit_code: 1
----- stdout -----
- bar.py:2:8: F401 [*] `bar` imported but unused
+ F401 [*] `bar` imported but unused
+ --> bar.py:2:8
|
2 | import bar # unused import
- | ^^^ F401
+ | ^^^
|
- = help: Remove unused import: `bar`
+ help: Remove unused import: `bar`
- foo.py:2:8: F401 [*] `foo` imported but unused
+ F401 [*] `foo` imported but unused
+ --> foo.py:2:8
|
2 | import foo # unused import
- | ^^^ F401
+ | ^^^
|
- = help: Remove unused import: `foo`
+ help: Remove unused import: `foo`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -208,12 +212,13 @@ fn check_warn_stdin_filename_with_files() {
success: false
exit_code: 1
----- stdout -----
- F401.py:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> F401.py:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -234,12 +239,13 @@ fn stdin_source_type_py() {
success: false
exit_code: 1
----- stdout -----
- TCH.py:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> TCH.py:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -471,10 +477,11 @@ fn stdin_fix_jupyter() {
"nbformat_minor": 5
}
----- stderr -----
- Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
+ F821 Undefined name `x`
+ --> Jupyter.ipynb:cell 3:1:7
|
1 | print(x)
- | ^ F821
+ | ^
|
Found 3 errors (2 fixed, 1 remaining).
@@ -569,19 +576,21 @@ fn stdin_override_parser_ipynb() {
success: false
exit_code: 1
----- stdout -----
- Jupyter.py:cell 1:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> Jupyter.py:cell 1:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
- Jupyter.py:cell 3:1:8: F401 [*] `sys` imported but unused
+ F401 [*] `sys` imported but unused
+ --> Jupyter.py:cell 3:1:8
|
1 | import sys
- | ^^^ F401
+ | ^^^
|
- = help: Remove unused import: `sys`
+ help: Remove unused import: `sys`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -605,12 +614,13 @@ fn stdin_override_parser_py() {
success: false
exit_code: 1
----- stdout -----
- F401.ipynb:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> F401.ipynb:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -633,12 +643,13 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
print(sys.version)
----- stderr -----
- -:3:4: F634 If test is a tuple, which is always `True`
+ F634 If test is a tuple, which is always `True`
+ --> -:3:4
|
1 | import sys
2 |
3 | if (1, 2):
- | ^^^^^^ F634
+ | ^^^^^^
4 | print(sys.version)
|
@@ -798,7 +809,8 @@ fn stdin_parse_error() {
success: false
exit_code: 1
----- stdout -----
- -:1:16: SyntaxError: Expected one or more symbol names after import
+ invalid-syntax: Expected one or more symbol names after import
+ --> -:1:16
|
1 | from foo import
| ^
@@ -818,14 +830,16 @@ fn stdin_multiple_parse_error() {
success: false
exit_code: 1
----- stdout -----
- -:1:16: SyntaxError: Expected one or more symbol names after import
+ invalid-syntax: Expected one or more symbol names after import
+ --> -:1:16
|
1 | from foo import
| ^
2 | bar =
|
- -:2:6: SyntaxError: Expected an expression
+ invalid-syntax: Expected an expression
+ --> -:2:6
|
1 | from foo import
2 | bar =
@@ -847,7 +861,8 @@ fn parse_error_not_included() {
success: false
exit_code: 1
----- stdout -----
- -:1:6: SyntaxError: Expected an expression
+ invalid-syntax: Expected an expression
+ --> -:1:6
|
1 | foo =
| ^
@@ -867,10 +882,11 @@ fn full_output_preview() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: E741 Ambiguous variable name: `l`
+ E741 Ambiguous variable name: `l`
+ --> -:1:1
|
1 | l = 1
- | ^ E741
+ | ^
|
Found 1 error.
@@ -895,10 +911,11 @@ preview = true
success: false
exit_code: 1
----- stdout -----
- -:1:1: E741 Ambiguous variable name: `l`
+ E741 Ambiguous variable name: `l`
+ --> -:1:1
|
1 | l = 1
- | ^ E741
+ | ^
|
Found 1 error.
@@ -916,10 +933,11 @@ fn full_output_format() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: E741 Ambiguous variable name: `l`
+ E741 Ambiguous variable name: `l`
+ --> -:1:1
|
1 | l = 1
- | ^ E741
+ | ^
|
Found 1 error.
@@ -1067,7 +1085,7 @@ fn show_statistics_syntax_errors() {
success: false
exit_code: 1
----- stdout -----
- 1 syntax-error
+ 1 invalid-syntax
Found 1 error.
----- stderr -----
@@ -1080,7 +1098,7 @@ fn show_statistics_syntax_errors() {
success: false
exit_code: 1
----- stdout -----
- 1 syntax-error
+ 1 invalid-syntax
Found 1 error.
----- stderr -----
@@ -1093,7 +1111,7 @@ fn show_statistics_syntax_errors() {
success: false
exit_code: 1
----- stdout -----
- 1 syntax-error
+ 1 invalid-syntax
Found 1 error.
----- stderr -----
@@ -1406,7 +1424,9 @@ fn redirect_direct() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF950 Hey this is a test rule that was redirected from another.
+ RUF950 Hey this is a test rule that was redirected from another.
+ --> -:1:1
+
Found 1 error.
----- stderr -----
@@ -1438,7 +1458,9 @@ fn redirect_prefix() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF950 Hey this is a test rule that was redirected from another.
+ RUF950 Hey this is a test rule that was redirected from another.
+ --> -:1:1
+
Found 1 error.
----- stderr -----
@@ -1455,7 +1477,9 @@ fn deprecated_direct() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF920 Hey this is a deprecated test rule.
+ RUF920 Hey this is a deprecated test rule.
+ --> -:1:1
+
Found 1 error.
----- stderr -----
@@ -1465,6 +1489,8 @@ fn deprecated_direct() {
#[test]
fn deprecated_multiple_direct() {
+ // Multiple deprecated rules selected by exact code should be included
+ // but a warning should be displayed
let mut cmd = RuffCheck::default()
.args(["--select", "RUF920", "--select", "RUF921"])
.build();
@@ -1472,8 +1498,12 @@ fn deprecated_multiple_direct() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF920 Hey this is a deprecated test rule.
- -:1:1: RUF921 Hey this is another deprecated test rule.
+ RUF920 Hey this is a deprecated test rule.
+ --> -:1:1
+
+ RUF921 Hey this is another deprecated test rule.
+ --> -:1:1
+
Found 2 errors.
----- stderr -----
@@ -1488,12 +1518,10 @@ fn deprecated_indirect() {
// since it is not a "direct" selection
let mut cmd = RuffCheck::default().args(["--select", "RUF92"]).build();
assert_cmd_snapshot!(cmd, @r"
- success: false
- exit_code: 1
+ success: true
+ exit_code: 0
----- stdout -----
- -:1:1: RUF920 Hey this is a deprecated test rule.
- -:1:1: RUF921 Hey this is another deprecated test rule.
- Found 2 errors.
+ All checks passed!
----- stderr -----
");
@@ -1638,27 +1666,49 @@ fn check_input_from_argfile() -> Result<()> {
(file_a_path.display().to_string().as_str(), "/path/to/a.py"),
]}, {
assert_cmd_snapshot!(cmd
- .pass_stdin(""), @r###"
+ .pass_stdin(""), @r"
success: false
exit_code: 1
----- stdout -----
- /path/to/a.py:1:8: F401 [*] `os` imported but unused
+ F401 [*] `os` imported but unused
+ --> /path/to/a.py:1:8
|
1 | import os
- | ^^ F401
+ | ^^
|
- = help: Remove unused import: `os`
+ help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
- "###);
+ ");
});
Ok(())
}
+#[test]
+// Regression test for https://github.com/astral-sh/ruff/issues/20655
+fn missing_argfile_reports_error() {
+ let mut cmd = RuffCheck::default().filename("@!.txt").build();
+ insta::with_settings!({filters => vec![
+ ("The system cannot find the file specified.", "No such file or directory")
+ ]}, {
+ assert_cmd_snapshot!(cmd, @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ ruff failed
+ Cause: Failed to read CLI arguments from files
+ Cause: failed to open file `!.txt`
+ Cause: No such file or directory (os error 2)
+ ");
+ });
+}
+
#[test]
fn check_hints_hidden_unsafe_fixes() {
let mut cmd = RuffCheck::default()
@@ -1669,8 +1719,12 @@ fn check_hints_hidden_unsafe_fixes() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1687,7 +1741,9 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1705,8 +1761,12 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
[*] 1 fixable with the --fix option.
@@ -1725,7 +1785,9 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 1 error.
----- stderr -----
@@ -1742,8 +1804,12 @@ fn check_shows_unsafe_fixes_with_opt_in() {
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 [*] Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
[*] 2 fixable with the --fix option.
@@ -1764,7 +1830,9 @@ fn fix_applies_safe_fixes_by_default() {
# fix from stable-test-rule-safe-fix
----- stderr -----
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
");
@@ -1801,7 +1869,9 @@ fn fix_does_not_apply_display_only_fixes() {
----- stdout -----
def add_to_list(item, some_list=[]): ...
----- stderr -----
- -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
+ RUF903 Hey this is a stable test rule with a display only fix.
+ --> -:1:1
+
Found 1 error.
");
}
@@ -1819,7 +1889,9 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
----- stdout -----
def add_to_list(item, some_list=[]): ...
----- stderr -----
- -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
+ RUF903 Hey this is a stable test rule with a display only fix.
+ --> -:1:1
+
Found 1 error.
");
}
@@ -1836,7 +1908,9 @@ fn fix_only_unsafe_fixes_available() {
----- stdout -----
----- stderr -----
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
");
@@ -1972,8 +2046,12 @@ extend-unsafe-fixes = ["RUF901"]
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF901 Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
@@ -2004,8 +2082,12 @@ extend-safe-fixes = ["RUF902"]
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 [*] Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -2038,8 +2120,12 @@ extend-safe-fixes = ["RUF902"]
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ RUF901 [*] Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2074,14 +2160,22 @@ extend-safe-fixes = ["RUF9"]
success: false
exit_code: 1
----- stdout -----
- -:1:1: RUF900 Hey this is a stable test rule.
- -:1:1: RUF901 Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
- -:1:1: RUF903 Hey this is a stable test rule with a display only fix.
- -:1:1: RUF920 Hey this is a deprecated test rule.
- -:1:1: RUF921 Hey this is another deprecated test rule.
- -:1:1: RUF950 Hey this is a test rule that was redirected from another.
- Found 7 errors.
+ RUF900 Hey this is a stable test rule.
+ --> -:1:1
+
+ RUF901 Hey this is a stable test rule with a safe fix.
+ --> -:1:1
+
+ RUF902 [*] Hey this is a stable test rule with an unsafe fix.
+ --> -:1:1
+
+ RUF903 Hey this is a stable test rule with a display only fix.
+ --> -:1:1
+
+ RUF950 Hey this is a test rule that was redirected from another.
+ --> -:1:1
+
+ Found 5 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -2141,10 +2235,11 @@ def log(x, base) -> float:
success: false
exit_code: 1
----- stdout -----
- -:2:5: D417 Missing argument description in the docstring for `log`: `base`
+ D417 Missing argument description in the docstring for `log`: `base`
+ --> -:2:5
|
2 | def log(x, base) -> float:
- | ^^^ D417
+ | ^^^
3 | """Calculate natural log of a value
|
@@ -2177,14 +2272,15 @@ select = ["RUF017"]
success: false
exit_code: 1
----- stdout -----
- -:3:1: RUF017 Avoid quadratic list summation
+ RUF017 Avoid quadratic list summation
+ --> -:3:1
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
- | ^^^^^^^^^^^^^^^ RUF017
+ | ^^^^^^^^^^^^^^^
|
- = help: Replace with `functools.reduce`
+ help: Replace with `functools.reduce`
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2217,14 +2313,15 @@ unfixable = ["RUF"]
success: false
exit_code: 1
----- stdout -----
- -:3:1: RUF017 Avoid quadratic list summation
+ RUF017 Avoid quadratic list summation
+ --> -:3:1
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
- | ^^^^^^^^^^^^^^^ RUF017
+ | ^^^^^^^^^^^^^^^
|
- = help: Replace with `functools.reduce`
+ help: Replace with `functools.reduce`
Found 1 error.
@@ -2246,11 +2343,11 @@ fn pyproject_toml_stdin_syntax_error() {
success: false
exit_code: 1
----- stdout -----
- pyproject.toml:1:9: RUF200 Failed to parse pyproject.toml: invalid table header
- expected `.`, `]`
+ RUF200 Failed to parse pyproject.toml: unclosed table, expected `]`
+ --> pyproject.toml:1:9
|
1 | [project
- | ^ RUF200
+ | ^
|
Found 1 error.
@@ -2272,11 +2369,12 @@ fn pyproject_toml_stdin_schema_error() {
success: false
exit_code: 1
----- stdout -----
- pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
+ RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
+ --> pyproject.toml:2:8
|
1 | [project]
2 | name = 1
- | ^ RUF200
+ | ^
|
Found 1 error.
@@ -2364,11 +2462,12 @@ fn pyproject_toml_stdin_schema_error_fix() {
[project]
name = 1
----- stderr -----
- pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
+ RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
+ --> pyproject.toml:2:8
|
1 | [project]
2 | name = 1
- | ^ RUF200
+ | ^
|
Found 1 error.
diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs
deleted file mode 100644
index ffba3a8e90..0000000000
--- a/crates/ruff/tests/lint.rs
+++ /dev/null
@@ -1,5694 +0,0 @@
-//! Tests the interaction of the `lint` configuration section
-
-#![cfg(not(target_family = "wasm"))]
-
-use regex::escape;
-use std::process::Command;
-use std::str;
-use std::{fs, path::Path};
-
-use anyhow::Result;
-use assert_fs::fixture::{ChildPath, FileTouch, PathChild};
-use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
-use tempfile::TempDir;
-
-const BIN_NAME: &str = "ruff";
-const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
-
-fn tempdir_filter(path: impl AsRef) -> String {
- format!(r"{}\\?/?", escape(path.as_ref().to_str().unwrap()))
-}
-
-#[test]
-fn top_level_options() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-
-[flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
- test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
- test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
- Found 3 errors.
- [*] 2 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- - 'flake8-quotes' -> 'lint.flake8-quotes'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn lint_options() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-extend-select = ["B", "Q"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:5: Q000 [*] Double quotes found but single quotes preferred
- -:1:5: B005 Using `.strip()` with multi-character strings is misleading
- -:1:19: Q000 [*] Double quotes found but single quotes preferred
- Found 3 errors.
- [*] 2 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Tests that configurations from the top-level and `lint` section are merged together.
-#[test]
-fn mixed_levels() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:5: Q000 [*] Double quotes found but single quotes preferred
- -:1:5: B005 Using `.strip()` with multi-character strings is misleading
- -:1:19: Q000 [*] Double quotes found but single quotes preferred
- Found 3 errors.
- [*] 2 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- ");
- });
-
- Ok(())
-}
-
-/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific).
-#[test]
-fn precedence() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-extend-select = ["B", "Q"]
-
-[flake8-quotes]
-inline-quotes = "double"
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"a = "abcba".strip("aba")"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:5: Q000 [*] Double quotes found but single quotes preferred
- -:1:5: B005 Using `.strip()` with multi-character strings is misleading
- -:1:19: Q000 [*] Double quotes found but single quotes preferred
- Found 3 errors.
- [*] 2 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
- - 'flake8-quotes' -> 'lint.flake8-quotes'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn exclude() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-extend-exclude = ["out"]
-
-[lint]
-exclude = ["test.py", "generated.py"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- fs::write(
- tempdir.path().join("main.py"),
- r#"
-from test import say_hy
-
-if __name__ == "__main__":
- say_hy("dear Ruff contributor")
-"#,
- )?;
-
- // Excluded file but passed to the CLI directly, should be linted
- let test_path = tempdir.path().join("test.py");
- fs::write(
- &test_path,
- r#"
-def say_hy(name: str):
- print(f"Hy {name}")"#,
- )?;
-
- fs::write(
- tempdir.path().join("generated.py"),
- r#"NUMBERS = [
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
- 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
-]
-OTHER = "OTHER"
-"#,
- )?;
-
- let out_dir = tempdir.path().join("out");
- fs::create_dir(&out_dir)?;
-
- fs::write(out_dir.join("a.py"), r#"a = "a""#)?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- // Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
- .arg(test_path.file_name().unwrap())
- // Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options
- .arg("."), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.py:4:16: Q000 [*] Double quotes found but single quotes preferred
- main.py:5:12: Q000 [*] Double quotes found but single quotes preferred
- test.py:3:15: Q000 [*] Double quotes found but single quotes preferred
- Found 3 errors.
- [*] 3 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn exclude_stdin() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-
-[lint]
-exclude = ["generated.py"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .args(["--stdin-filename", "generated.py"])
- .arg("-")
- .pass_stdin(r#"
-from test import say_hy
-
-if __name__ == "__main__":
- say_hy("dear Ruff contributor")
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred
- generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred
- Found 2 errors.
- [*] 2 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn line_too_long_width_override() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-line-length = 80
-select = ["E501"]
-
-[pycodestyle]
-max-line-length = 100
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"
-# longer than 80, but less than 100
-_ = "---------------------------------------------------------------------------亜亜亜亜亜亜"
-# longer than 100
-_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜"
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:5:91: E501 Line too long (109 > 100)
- Found 1 error.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
- - 'select' -> 'lint.select'
- - 'pycodestyle' -> 'lint.pycodestyle'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn per_file_ignores_stdin() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .args(["--stdin-filename", "generated.py"])
- .args(["--per-file-ignores", "generated.py:Q"])
- .arg("-")
- .pass_stdin(r#"
-import os
-
-from test import say_hy
-
-if __name__ == "__main__":
- say_hy("dear Ruff contributor")
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- generated.py:2:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn extend_per_file_ignores_stdin() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend-select = ["B", "Q"]
-
-[lint.flake8-quotes]
-inline-quotes = "single"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .args(["--stdin-filename", "generated.py"])
- .args(["--extend-per-file-ignores", "generated.py:Q"])
- .arg("-")
- .pass_stdin(r#"
-import os
-
-from test import say_hy
-
-if __name__ == "__main__":
- say_hy("dear Ruff contributor")
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- generated.py:2:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
- - 'extend-select' -> 'lint.extend-select'
- ");
- });
-
- Ok(())
-}
-
-/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858)
-#[test]
-fn parent_configuration_override() -> Result<()> {
- let tempdir = TempDir::new()?;
- let root_ruff = tempdir.path().join("ruff.toml");
- fs::write(
- root_ruff,
- r#"
-[lint]
-select = ["ALL"]
-"#,
- )?;
-
- let sub_dir = tempdir.path().join("subdirectory");
- fs::create_dir(&sub_dir)?;
-
- let subdirectory_ruff = sub_dir.join("ruff.toml");
- fs::write(
- subdirectory_ruff,
- r#"
-[lint]
-ignore = ["D203", "D212"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(sub_dir)
- .args(STDIN_BASE_OPTIONS)
- , @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- warning: No Python files found under the given path(s)
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn nonexistent_config_file() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "foo.toml", "."]), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'foo.toml' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- It looks like you were trying to pass a path to a configuration file.
- The path `foo.toml` does not point to a configuration file
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn config_override_rejected_if_invalid_toml() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "foo = bar", "."]), @r#"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'foo = bar' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- The supplied argument is not valid TOML:
-
- TOML parse error at line 1, column 7
- |
- 1 | foo = bar
- | ^
- invalid string
- expected `"`, `'`
-
- For more information, try '--help'.
- "#);
-}
-
-#[test]
-fn too_many_config_files() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_dot_toml = tempdir.path().join("ruff.toml");
- let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
- fs::File::create(&ruff_dot_toml)?;
- fs::File::create(&ruff2_dot_toml)?;
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_dot_toml)
- .arg("--config")
- .arg(&ruff2_dot_toml)
- .arg("."), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: You cannot specify more than one configuration file on the command line.
-
- tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
- For more information, try `--help`.
- ");
- });
- Ok(())
-}
-
-#[test]
-fn extend_passed_via_config_argument() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "extend = 'foo.toml'", "."]), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'extend = 'foo.toml'' for '--config '
-
- tip: Cannot include `extend` in a --config flag value
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn nonexistent_extend_file() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- fs::write(
- project_dir.join("ruff.toml"),
- r#"
-extend = "ruff2.toml"
-"#,
- )?;
-
- fs::write(
- project_dir.join("ruff2.toml"),
- r#"
-extend = "ruff3.toml"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![
- (tempdir_filter(&project_dir).as_str(), "[TMP]/"),
- ("The system cannot find the file specified.", "No such file or directory")
- ]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(["check"]).current_dir(project_dir), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Failed to load extended configuration `[TMP]/ruff3.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml`)
- Cause: Failed to read [TMP]/ruff3.toml
- Cause: No such file or directory (os error 2)
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn circular_extend() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_path = dunce::canonicalize(tempdir.path())?;
-
- fs::write(
- project_path.join("ruff.toml"),
- r#"
-extend = "ruff2.toml"
-"#,
- )?;
- fs::write(
- project_path.join("ruff2.toml"),
- r#"
-extend = "ruff3.toml"
-"#,
- )?;
- fs::write(
- project_path.join("ruff3.toml"),
- r#"
-extend = "ruff.toml"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(["check"])
- .current_dir(project_path),
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Circular configuration detected: `[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml` extends `[TMP]/ruff.toml`
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn parse_error_extends() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_path = dunce::canonicalize(tempdir.path())?;
-
- fs::write(
- project_path.join("ruff.toml"),
- r#"
-extend = "ruff2.toml"
-"#,
- )?;
- fs::write(
- project_path.join("ruff2.toml"),
- r#"
-[lint]
-select = [E501]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(["check"])
- .current_dir(project_path),
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Failed to load extended configuration `[TMP]/ruff2.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml`)
- Cause: Failed to parse [TMP]/ruff2.toml
- Cause: TOML parse error at line 3, column 11
- |
- 3 | select = [E501]
- | ^
- invalid array
- expected `]`
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn config_file_and_isolated() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_dot_toml = tempdir.path().join("ruff.toml");
- fs::File::create(&ruff_dot_toml)?;
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_dot_toml)
- .arg("--isolated")
- .arg("."), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
-
- tip: You cannot specify a configuration file and also specify `--isolated`,
- as `--isolated` causes ruff to ignore all configuration files.
- For more information, try `--help`.
- ");
- });
- Ok(())
-}
-
-#[test]
-fn config_override_via_cli() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-line-length = 100
-
-[lint]
-select = ["I"]
-
-[lint.isort]
-combine-as-imports = true
- "#,
- )?;
- let fixture = r#"
-from foo import (
- aaaaaaaaaaaaaaaaaaa,
- bbbbbbbbbbb as bbbbbbbbbbbbbbbb,
- cccccccccccccccc,
- ddddddddddd as ddddddddddddd,
- eeeeeeeeeeeeeee,
- ffffffffffff as ffffffffffffff,
- ggggggggggggg,
- hhhhhhh as hhhhhhhhhhh,
- iiiiiiiiiiiiii,
- jjjjjjjjjjjjj as jjjjjj,
-)
-
-x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
-"#;
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--config", "line-length=90"])
- .args(["--config", "lint.extend-select=['E501', 'F841']"])
- .args(["--config", "lint.isort.combine-as-imports = false"])
- .arg("-")
- .pass_stdin(fixture), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:2:1: I001 [*] Import block is un-sorted or un-formatted
- -:15:91: E501 Line too long (97 > 90)
- Found 2 errors.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-#[test]
-fn valid_toml_but_nonexistent_option_provided_via_config_argument() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args([".", "--config", "extend-select=['F481']"]), // No such code as F481!
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'extend-select=['F481']' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- Could not parse the supplied argument as a `ruff.toml` configuration option:
-
- Unknown rule selector: `F481`
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn each_toml_option_requires_a_new_flag_1() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- // commas can't be used to delimit different config overrides;
- // you need a new --config flag for each override
- .args([".", "--config", "extend-select=['F841'], line-length=90"]),
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'extend-select=['F841'], line-length=90' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- The supplied argument is not valid TOML:
-
- TOML parse error at line 1, column 23
- |
- 1 | extend-select=['F841'], line-length=90
- | ^
- expected newline, `#`
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn each_toml_option_requires_a_new_flag_2() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- // spaces *also* can't be used to delimit different config overrides;
- // you need a new --config flag for each override
- .args([".", "--config", "extend-select=['F841'] line-length=90"]),
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'extend-select=['F841'] line-length=90' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- The supplied argument is not valid TOML:
-
- TOML parse error at line 1, column 24
- |
- 1 | extend-select=['F841'] line-length=90
- | ^
- expected newline, `#`
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn value_given_to_table_key_is_not_inline_table_1() {
- // https://github.com/astral-sh/ruff/issues/13995
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args([".", "--config", r#"lint.flake8-pytest-style="csv""#]),
- @r#"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'lint.flake8-pytest-style="csv"' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- `lint.flake8-pytest-style` is a table of configuration options.
- Did you want to override one of the table's subkeys?
-
- Possible choices:
-
- - `lint.flake8-pytest-style.fixture-parentheses`
- - `lint.flake8-pytest-style.parametrize-names-type`
- - `lint.flake8-pytest-style.parametrize-values-type`
- - `lint.flake8-pytest-style.parametrize-values-row-type`
- - `lint.flake8-pytest-style.raises-require-match-for`
- - `lint.flake8-pytest-style.raises-extend-require-match-for`
- - `lint.flake8-pytest-style.mark-parentheses`
- - `lint.flake8-pytest-style.warns-require-match-for`
- - `lint.flake8-pytest-style.warns-extend-require-match-for`
-
- For more information, try '--help'.
- "#);
-}
-
-#[test]
-fn value_given_to_table_key_is_not_inline_table_2() {
- // https://github.com/astral-sh/ruff/issues/13995
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args([".", "--config", r#"lint=123"#]),
- @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: invalid value 'lint=123' for '--config '
-
- tip: A `--config` flag must either be a path to a `.toml` configuration file
- or a TOML ` = ` pair overriding a specific configuration
- option
-
- `lint` is a table of configuration options.
- Did you want to override one of the table's subkeys?
-
- Possible choices:
-
- - `lint.allowed-confusables`
- - `lint.dummy-variable-rgx`
- - `lint.extend-ignore`
- - `lint.extend-select`
- - `lint.extend-fixable`
- - `lint.external`
- - `lint.fixable`
- - `lint.ignore`
- - `lint.extend-safe-fixes`
- - `lint.extend-unsafe-fixes`
- - `lint.ignore-init-module-imports`
- - `lint.logger-objects`
- - `lint.select`
- - `lint.explicit-preview-rules`
- - `lint.task-tags`
- - `lint.typing-modules`
- - `lint.unfixable`
- - `lint.per-file-ignores`
- - `lint.extend-per-file-ignores`
- - `lint.exclude`
- - `lint.preview`
- - `lint.typing-extensions`
-
- For more information, try '--help'.
- ");
-}
-
-#[test]
-fn config_doubly_overridden_via_cli() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-line-length = 100
-
-[lint]
-select=["E501"]
-"#,
- )?;
- let fixture = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'";
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- // The --line-length flag takes priority over both the config file
- // and the `--config="line-length=110"` flag,
- // despite them both being specified after this flag on the command line:
- .args(["--line-length", "90"])
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--config", "line-length=110"])
- .arg("-")
- .pass_stdin(fixture), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:91: E501 Line too long (97 > 90)
- Found 1 error.
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-#[test]
-fn complex_config_setting_overridden_via_cli() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(&ruff_toml, "lint.select = ['N801']")?;
- let fixture = "class violates_n801: pass";
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"])
- .args(["--stdin-filename", "generated.py"])
- .arg("-")
- .pass_stdin(fixture), @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-#[test]
-fn deprecated_config_option_overridden_via_cli() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "select=['N801']", "-"])
- .pass_stdin("class lowercase: ..."),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:7: N801 Class name `lowercase` should use CapWords convention
- Found 1 error.
-
- ----- stderr -----
- warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your `--config` CLI arguments:
- - 'select' -> 'lint.select'
- ");
-}
-
-#[test]
-fn extension() -> Result<()> {
- let tempdir = TempDir::new()?;
-
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-include = ["*.ipy"]
-"#,
- )?;
-
- fs::write(
- tempdir.path().join("main.ipy"),
- r#"
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
- "metadata": {},
- "outputs": [],
- "source": [
- "import os"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.12.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .args(["--extension", "ipy:ipynb"])
- .arg("."), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.ipy:cell 1:1:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn warn_invalid_noqa_with_no_diagnostics() {
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--isolated"])
- .arg("--select")
- .arg("F401")
- .arg("-")
- .pass_stdin(
- r#"
-# ruff: noqa: AAA101
-print("Hello world!")
-"#
- )
- );
-}
-
-#[test]
-fn file_noqa_external() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-external = ["AAA"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"
-# flake8: noqa: AAA101, BBB102
-import os
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:3:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn required_version_exact_mismatch() -> Result<()> {
- let version = env!("CARGO_PKG_VERSION");
-
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-required-version = "0.1.0"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"
-import os
-"#), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn required_version_exact_match() -> Result<()> {
- let version = env!("CARGO_PKG_VERSION");
-
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- format!(
- r#"
-required-version = "{version}"
-"#
- ),
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"
-import os
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:2:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn required_version_bound_mismatch() -> Result<()> {
- let version = env!("CARGO_PKG_VERSION");
-
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- format!(
- r#"
-required-version = ">{version}"
-"#
- ),
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"
-import os
-"#), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn required_version_bound_match() -> Result<()> {
- let version = env!("CARGO_PKG_VERSION");
-
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-required-version = ">=0.1.0"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("-")
- .pass_stdin(r#"
-import os
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:2:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Expand environment variables in `--config` paths provided via the CLI.
-#[test]
-fn config_expand() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- ruff_toml,
- r#"
-[lint]
-select = ["F"]
-ignore = ["F841"]
-"#,
- )?;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg("${NAME}.toml")
- .env("NAME", "ruff")
- .arg("-")
- .current_dir(tempdir.path())
- .pass_stdin(r#"
-import os
-
-def func():
- x = 1
-"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:2:8: F401 [*] `os` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
-
- Ok(())
-}
-
-/// Per-file selects via ! negation in per-file-ignores
-#[test]
-fn negated_per_file_ignores() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.per-file-ignores]
-"!selected.py" = ["RUF"]
-"#,
- )?;
- let selected = tempdir.path().join("selected.py");
- fs::write(selected, "")?;
- let ignored = tempdir.path().join("ignored.py");
- fs::write(ignored, "")?;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("--select")
- .arg("RUF901")
- .current_dir(&tempdir)
- , @r"
- success: false
- exit_code: 1
- ----- stdout -----
- selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-#[test]
-fn negated_per_file_ignores_absolute() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.per-file-ignores]
-"!src/**.py" = ["RUF"]
-"#,
- )?;
- let src_dir = tempdir.path().join("src");
- fs::create_dir(&src_dir)?;
- let selected = src_dir.join("selected.py");
- fs::write(selected, "")?;
- let ignored = tempdir.path().join("ignored.py");
- fs::write(ignored, "")?;
-
- insta::with_settings!({filters => vec![(r"\\", "/")]}, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("--select")
- .arg("RUF901")
- .current_dir(&tempdir)
- , @r"
- success: false
- exit_code: 1
- ----- stdout -----
- src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
- Ok(())
-}
-
-/// patterns are additive, can't use negative patterns to "un-ignore"
-#[test]
-fn negated_per_file_ignores_overlap() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.per-file-ignores]
-"*.py" = ["RUF"]
-"!foo.py" = ["RUF"]
-"#,
- )?;
- let foo_file = tempdir.path().join("foo.py");
- fs::write(foo_file, "")?;
- let bar_file = tempdir.path().join("bar.py");
- fs::write(bar_file, "")?;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .arg("--select")
- .arg("RUF901")
- .current_dir(&tempdir)
- , @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-#[test]
-fn unused_interaction() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["F"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("--fix")
- .arg("-")
- .pass_stdin(r#"
-import os # F401
-
-def function():
- import os # F811
- print(os.name)
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- import os # F401
-
- def function():
- print(os.name)
-
- ----- stderr -----
- Found 1 error (1 fixed, 0 remaining).
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn add_noqa() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["RUF015"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-def first_square():
- return [x * x for x in range(20)][0]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .arg(&test_path)
- .args(["--add-noqa"])
- .arg("-")
- .pass_stdin(r#"
-
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
-
- insta::assert_snapshot!(test_code, @r"
- def first_square():
- return [x * x for x in range(20)][0] # noqa: RUF015
- ");
-
- Ok(())
-}
-
-#[test]
-fn add_noqa_multiple_codes() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["ANN001", "ANN201", "ARG001", "D103"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-def unused(x):
- pass
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .arg(&test_path)
- .arg("--preview")
- .args(["--add-noqa"])
- .arg("-")
- .pass_stdin(r#"
-
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
-
- insta::assert_snapshot!(test_code, @r"
- def unused(x): # noqa: ANN001, ANN201, D103
- pass
- ");
-
- Ok(())
-}
-
-#[test]
-fn add_noqa_multiline_diagnostic() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["I"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-import z
-import c
-import a
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .arg(&test_path)
- .args(["--add-noqa"])
- .arg("-")
- .pass_stdin(r#"
-
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
-
- insta::assert_snapshot!(test_code, @r"
- import z # noqa: I001
- import c
- import a
- ");
-
- Ok(())
-}
-
-#[test]
-fn add_noqa_existing_noqa() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["ANN001", "ANN201", "ARG001", "D103"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-def unused(x): # noqa: ANN001, ARG001, D103
- pass
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .arg(&test_path)
- .arg("--preview")
- .args(["--add-noqa"])
- .arg("-")
- .pass_stdin(r#"
-
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
-
- insta::assert_snapshot!(test_code, @r"
- def unused(x): # noqa: ANN001, ANN201, ARG001, D103
- pass
- ");
-
- Ok(())
-}
-
-#[test]
-fn add_noqa_multiline_comment() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["UP031"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-print(
- """First line
- second line
- third line
- %s"""
- % name
-)
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
- .arg(&test_path)
- .arg("--preview")
- .args(["--add-noqa"])
- .arg("-")
- .pass_stdin(r#"
-
-"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
-
- insta::assert_snapshot!(test_code, @r#"
- print(
- """First line
- second line
- third line
- %s""" # noqa: UP031
- % name
- )
- "#);
-
- Ok(())
-}
-
-#[test]
-fn add_noqa_exclude() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-exclude = ["excluded.py"]
-select = ["RUF015"]
-"#,
- )?;
-
- let test_path = tempdir.path().join("noqa.py");
-
- fs::write(
- &test_path,
- r#"
-def first_square():
- return [x * x for x in range(20)][0]
-"#,
- )?;
-
- let exclude_path = tempdir.path().join("excluded.py");
-
- fs::write(
- &exclude_path,
- r#"
-def first_square():
- return [x * x for x in range(20)][0]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(tempdir.path())
- .args(STDIN_BASE_OPTIONS)
- .args(["--add-noqa"]), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- Added 1 noqa directive.
- ");
- });
-
- Ok(())
-}
-
-/// Regression test for
-#[test]
-fn add_noqa_parent() -> Result<()> {
- let tempdir = TempDir::new()?;
- let test_path = tempdir.path().join("noqa.py");
- fs::write(
- &test_path,
- r#"
-from foo import ( # noqa: F401
- bar
-)
- "#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--add-noqa")
- .arg("--select=F401")
- .arg("noqa.py")
- .current_dir(&tempdir), @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Infer `3.11` from `requires-python` in `pyproject.toml`.
-#[test]
-fn requires_python() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &ruff_toml,
- r#"[project]
-requires-python = ">= 3.11"
-
-[tool.ruff.lint]
-select = ["UP006"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.8"
-
-[tool.ruff.lint]
-select = ["UP006"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&pyproject_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Infer `3.11` from `requires-python` in `pyproject.toml`.
-#[test]
-fn requires_python_patch() -> Result<()> {
- let tempdir = TempDir::new()?;
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.11.4"
-
-[tool.ruff.lint]
-select = ["UP006"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&pyproject_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Infer `3.11` from `requires-python` in `pyproject.toml`.
-#[test]
-fn requires_python_equals() -> Result<()> {
- let tempdir = TempDir::new()?;
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = "== 3.11"
-
-[tool.ruff.lint]
-select = ["UP006"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&pyproject_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Infer `3.11` from `requires-python` in `pyproject.toml`.
-#[test]
-fn requires_python_equals_patch() -> Result<()> {
- let tempdir = TempDir::new()?;
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = "== 3.11.4"
-
-[tool.ruff.lint]
-select = ["UP006"]
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&pyproject_toml)
- .args(["--stdin-filename", "test.py"])
- .arg("-")
- .pass_stdin(r#"from typing import List; foo: List[int]"#), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── pyproject.toml #<--- no `[tool.ruff]`
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_no_tool() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &ruff_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"from typing import Union;foo: Union[int, str] = 1"#,
- )?;
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .args(["--select","UP007"])
- .arg("test.py")
- .arg("-")
- .current_dir(project_dir)
- , @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/test.py"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.11
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.11
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.11
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── pyproject.toml #<--- no `[tool.ruff]`
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_no_tool_target_version_override() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &ruff_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"from typing import Union;foo: Union[int, str] = 1"#,
- )?;
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .args(["--select","UP007"])
- .args(["--target-version","py310"])
- .arg("test.py")
- .arg("-")
- .current_dir(project_dir)
- , @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/test.py"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.10
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.10
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.10
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-/// ```
-/// tmp
-/// ├── pyproject.toml #<--- no `[tool.ruff]`
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_no_tool_with_check() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &ruff_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"from typing import Union;foo: Union[int, str] = 1"#,
- )?;
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--select","UP007"])
- .arg(".")
- .current_dir(project_dir)
- , @r###"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:31: UP007 [*] Use `X | Y` for type annotations
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- "###);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── pyproject.toml #<-- no [tool.ruff]
-/// ├── ruff.toml #<-- no `target-version`
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_ruff_toml_no_target_fallback() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"[lint]
-select = ["UP007"]
-"#,
- )?;
-
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("test.py")
- .arg("--show-settings")
- .current_dir(project_dir), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/test.py"
- Settings path: "[TMP]/ruff.toml"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.11
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.11
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.11
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── pyproject.toml #<-- no [tool.ruff]
-/// ├── ruff.toml #<-- no `target-version`
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"[lint]
-select = ["UP007"]
-"#,
- )?;
-
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg(".")
- .current_dir(project_dir), @r###"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:2:31: UP007 [*] Use `X | Y` for type annotations
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- "###);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── foo
-/// │ ├── pyproject.toml #<-- no [tool.ruff], no `requires-python`
-/// │ └── test.py
-/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python`
-/// ```
-#[test]
-fn requires_python_pyproject_toml_above() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let outer_pyproject = tempdir.path().join("pyproject.toml");
- fs::write(
- &outer_pyproject,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let foodir = tempdir.path().join("foo");
- fs::create_dir(foodir)?;
-
- let inner_pyproject = tempdir.path().join("foo/pyproject.toml");
- fs::write(
- &inner_pyproject,
- r#"[project]
-"#,
- )?;
-
- let testpy = tempdir.path().join("foo/test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- let testpy_canon = dunce::canonicalize(testpy)?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"(?m)^foo\\test","foo/test")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .args(["--select","UP007"])
- .arg("foo/test.py")
- .current_dir(&project_dir), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/foo/test.py"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.11
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.11
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.11
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── foo
-/// │ ├── pyproject.toml #<-- has [tool.ruff], no `requires-python`
-/// │ └── test.py
-/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python`
-/// ```
-#[test]
-fn requires_python_pyproject_toml_above_with_tool() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let outer_pyproject = tempdir.path().join("pyproject.toml");
- fs::write(
- &outer_pyproject,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let foodir = tempdir.path().join("foo");
- fs::create_dir(foodir)?;
-
- let inner_pyproject = tempdir.path().join("foo/pyproject.toml");
- fs::write(
- &inner_pyproject,
- r#"
-[tool.ruff]
-target-version = "py310"
-"#,
- )?;
-
- let testpy = tempdir.path().join("foo/test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- let testpy_canon = dunce::canonicalize(testpy)?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"foo\\","foo/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .args(["--select","UP007"])
- .arg("foo/test.py")
- .current_dir(&project_dir), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/foo/test.py"
-
- # General Settings
- cache_dir = "[TMP]/foo/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/foo"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/foo"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.10
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/foo",
- "[TMP]/foo/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.10
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.10
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── foo
-/// │ ├── pyproject.toml #<-- no [tool.ruff]
-/// │ └── test.py
-/// └── ruff.toml #<-- no `target-version`
-/// ```
-#[test]
-fn requires_python_ruff_toml_above() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint]
-select = ["UP007"]
-"#,
- )?;
-
- let foodir = tempdir.path().join("foo");
- fs::create_dir(foodir)?;
-
- let pyproject_toml = tempdir.path().join("foo/pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.11"
-"#,
- )?;
-
- let testpy = tempdir.path().join("foo/test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- let testpy_canon = dunce::canonicalize(testpy)?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .arg("foo/test.py")
- .current_dir(&project_dir), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/foo/test.py"
- Settings path: "[TMP]/ruff.toml"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = none
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.9
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.9
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .arg("test.py")
- .current_dir(project_dir.join("foo")), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/foo/test.py"
- Settings path: "[TMP]/ruff.toml"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = none
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.9
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.9
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
- Ok(())
-}
-
-/// ```
-/// tmp
-/// ├── pyproject.toml <-- requires >=3.10
-/// ├── ruff.toml <--- extends base
-/// ├── shared
-/// │ └── base_config.toml <-- targets 3.11
-/// └── test.py
-/// ```
-#[test]
-fn requires_python_extend_from_shared_config() -> Result<()> {
- let tempdir = TempDir::new()?;
- let project_dir = dunce::canonicalize(tempdir.path())?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-extend = "./shared/base_config.toml"
-[lint]
-select = ["UP007"]
-"#,
- )?;
-
- let shared_dir = tempdir.path().join("shared");
- fs::create_dir(shared_dir)?;
-
- let pyproject_toml = tempdir.path().join("pyproject.toml");
- fs::write(
- &pyproject_toml,
- r#"[project]
-requires-python = ">= 3.10"
-"#,
- )?;
-
- let shared_toml = tempdir.path().join("shared/base_config.toml");
- fs::write(
- &shared_toml,
- r#"
-target-version = "py311"
-"#,
- )?;
-
- let testpy = tempdir.path().join("test.py");
- fs::write(
- &testpy,
- r#"
-from typing import Union;foo: Union[int, str] = 1
-"#,
- )?;
-
- let testpy_canon = dunce::canonicalize(testpy)?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--show-settings")
- .arg("test.py")
- .current_dir(&project_dir), @r#"
- success: true
- exit_code: 0
- ----- stdout -----
- Resolved settings for: "[TMP]/test.py"
- Settings path: "[TMP]/ruff.toml"
-
- # General Settings
- cache_dir = "[TMP]/.ruff_cache"
- fix = false
- fix_only = false
- output_format = concise
- show_fixes = false
- unsafe_fixes = hint
-
- # File Resolver Settings
- file_resolver.exclude = [
- ".bzr",
- ".direnv",
- ".eggs",
- ".git",
- ".git-rewrite",
- ".hg",
- ".ipynb_checkpoints",
- ".mypy_cache",
- ".nox",
- ".pants.d",
- ".pyenv",
- ".pytest_cache",
- ".pytype",
- ".ruff_cache",
- ".svn",
- ".tox",
- ".venv",
- ".vscode",
- "__pypackages__",
- "_build",
- "buck-out",
- "dist",
- "node_modules",
- "site-packages",
- "venv",
- ]
- file_resolver.extend_exclude = []
- file_resolver.force_exclude = false
- file_resolver.include = [
- "*.py",
- "*.pyi",
- "*.ipynb",
- "**/pyproject.toml",
- ]
- file_resolver.extend_include = []
- file_resolver.respect_gitignore = true
- file_resolver.project_root = "[TMP]/"
-
- # Linter Settings
- linter.exclude = []
- linter.project_root = "[TMP]/"
- linter.rules.enabled = [
- non-pep604-annotation-union (UP007),
- ]
- linter.rules.should_fix = [
- non-pep604-annotation-union (UP007),
- ]
- linter.per_file_ignores = {}
- linter.safety_table.forced_safe = []
- linter.safety_table.forced_unsafe = []
- linter.unresolved_target_version = 3.10
- linter.per_file_target_version = {}
- linter.preview = disabled
- linter.explicit_preview_rules = false
- linter.extension = ExtensionMapping({})
- linter.allowed_confusables = []
- linter.builtins = []
- linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
- linter.external = []
- linter.ignore_init_module_imports = true
- linter.logger_objects = []
- linter.namespace_packages = []
- linter.src = [
- "[TMP]/",
- "[TMP]/src",
- ]
- linter.tab_size = 4
- linter.line_length = 88
- linter.task_tags = [
- TODO,
- FIXME,
- XXX,
- ]
- linter.typing_modules = []
- linter.typing_extensions = true
-
- # Linter Plugins
- linter.flake8_annotations.mypy_init_return = false
- linter.flake8_annotations.suppress_dummy_args = false
- linter.flake8_annotations.suppress_none_returning = false
- linter.flake8_annotations.allow_star_arg_any = false
- linter.flake8_annotations.ignore_fully_untyped = false
- linter.flake8_bandit.hardcoded_tmp_directory = [
- /tmp,
- /var/tmp,
- /dev/shm,
- ]
- linter.flake8_bandit.check_typed_exception = false
- linter.flake8_bandit.extend_markup_names = []
- linter.flake8_bandit.allowed_markup_calls = []
- linter.flake8_bugbear.extend_immutable_calls = []
- linter.flake8_builtins.allowed_modules = []
- linter.flake8_builtins.ignorelist = []
- linter.flake8_builtins.strict_checking = false
- linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
- linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
- linter.flake8_copyright.author = none
- linter.flake8_copyright.min_file_size = 0
- linter.flake8_errmsg.max_string_length = 0
- linter.flake8_gettext.functions_names = [
- _,
- gettext,
- ngettext,
- ]
- linter.flake8_implicit_str_concat.allow_multiline = true
- linter.flake8_import_conventions.aliases = {
- altair = alt,
- holoviews = hv,
- matplotlib = mpl,
- matplotlib.pyplot = plt,
- networkx = nx,
- numpy = np,
- numpy.typing = npt,
- pandas = pd,
- panel = pn,
- plotly.express = px,
- polars = pl,
- pyarrow = pa,
- seaborn = sns,
- tensorflow = tf,
- tkinter = tk,
- xml.etree.ElementTree = ET,
- }
- linter.flake8_import_conventions.banned_aliases = {}
- linter.flake8_import_conventions.banned_from = []
- linter.flake8_pytest_style.fixture_parentheses = false
- linter.flake8_pytest_style.parametrize_names_type = tuple
- linter.flake8_pytest_style.parametrize_values_type = list
- linter.flake8_pytest_style.parametrize_values_row_type = tuple
- linter.flake8_pytest_style.raises_require_match_for = [
- BaseException,
- Exception,
- ValueError,
- OSError,
- IOError,
- EnvironmentError,
- socket.error,
- ]
- linter.flake8_pytest_style.raises_extend_require_match_for = []
- linter.flake8_pytest_style.mark_parentheses = false
- linter.flake8_quotes.inline_quotes = double
- linter.flake8_quotes.multiline_quotes = double
- linter.flake8_quotes.docstring_quotes = double
- linter.flake8_quotes.avoid_escape = true
- linter.flake8_self.ignore_names = [
- _make,
- _asdict,
- _replace,
- _fields,
- _field_defaults,
- _name_,
- _value_,
- ]
- linter.flake8_tidy_imports.ban_relative_imports = "parents"
- linter.flake8_tidy_imports.banned_api = {}
- linter.flake8_tidy_imports.banned_module_level_imports = []
- linter.flake8_type_checking.strict = false
- linter.flake8_type_checking.exempt_modules = [
- typing,
- typing_extensions,
- ]
- linter.flake8_type_checking.runtime_required_base_classes = []
- linter.flake8_type_checking.runtime_required_decorators = []
- linter.flake8_type_checking.quote_annotations = false
- linter.flake8_unused_arguments.ignore_variadic_names = false
- linter.isort.required_imports = []
- linter.isort.combine_as_imports = false
- linter.isort.force_single_line = false
- linter.isort.force_sort_within_sections = false
- linter.isort.detect_same_package = true
- linter.isort.case_sensitive = false
- linter.isort.force_wrap_aliases = false
- linter.isort.force_to_top = []
- linter.isort.known_modules = {}
- linter.isort.order_by_type = true
- linter.isort.relative_imports_order = furthest_to_closest
- linter.isort.single_line_exclusions = []
- linter.isort.split_on_trailing_comma = true
- linter.isort.classes = []
- linter.isort.constants = []
- linter.isort.variables = []
- linter.isort.no_lines_before = []
- linter.isort.lines_after_imports = -1
- linter.isort.lines_between_types = 0
- linter.isort.forced_separate = []
- linter.isort.section_order = [
- known { type = future },
- known { type = standard_library },
- known { type = third_party },
- known { type = first_party },
- known { type = local_folder },
- ]
- linter.isort.default_section = known { type = third_party }
- linter.isort.no_sections = false
- linter.isort.from_first = false
- linter.isort.length_sort = false
- linter.isort.length_sort_straight = false
- linter.mccabe.max_complexity = 10
- linter.pep8_naming.ignore_names = [
- setUp,
- tearDown,
- setUpClass,
- tearDownClass,
- setUpModule,
- tearDownModule,
- asyncSetUp,
- asyncTearDown,
- setUpTestData,
- failureException,
- longMessage,
- maxDiff,
- ]
- linter.pep8_naming.classmethod_decorators = []
- linter.pep8_naming.staticmethod_decorators = []
- linter.pycodestyle.max_line_length = 88
- linter.pycodestyle.max_doc_length = none
- linter.pycodestyle.ignore_overlong_task_comments = false
- linter.pyflakes.extend_generics = []
- linter.pyflakes.allowed_unused_imports = []
- linter.pylint.allow_magic_value_types = [
- str,
- bytes,
- ]
- linter.pylint.allow_dunder_method_names = []
- linter.pylint.max_args = 5
- linter.pylint.max_positional_args = 5
- linter.pylint.max_returns = 6
- linter.pylint.max_bool_expr = 5
- linter.pylint.max_branches = 12
- linter.pylint.max_statements = 50
- linter.pylint.max_public_methods = 20
- linter.pylint.max_locals = 15
- linter.pyupgrade.keep_runtime_typing = false
- linter.ruff.parenthesize_tuple_in_subscript = false
-
- # Formatter Settings
- formatter.exclude = []
- formatter.unresolved_target_version = 3.10
- formatter.per_file_target_version = {}
- formatter.preview = disabled
- formatter.line_width = 88
- formatter.line_ending = auto
- formatter.indent_style = space
- formatter.indent_width = 4
- formatter.quote_style = double
- formatter.magic_trailing_comma = respect
- formatter.docstring_code_format = disabled
- formatter.docstring_code_line_width = dynamic
-
- # Analyze Settings
- analyze.exclude = []
- analyze.preview = disabled
- analyze.target_version = 3.10
- analyze.detect_string_imports = false
- analyze.extension = ExtensionMapping({})
- analyze.include_dependencies = {}
-
- ----- stderr -----
- "#);
- });
-
- Ok(())
-}
-
-#[test]
-fn checks_notebooks_in_stable() -> anyhow::Result<()> {
- let tempdir = TempDir::new()?;
- std::fs::write(
- tempdir.path().join("main.ipynb"),
- r#"
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
- "metadata": {},
- "outputs": [],
- "source": [
- "import random"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.12.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
-"#,
- )?;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--select")
- .arg("F401")
- .current_dir(&tempdir)
- , @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.ipynb:cell 1:1:8: F401 [*] `random` imported but unused
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- Ok(())
-}
-
-/// Verify that implicit namespace packages are detected even when they are nested.
-///
-/// See:
-#[test]
-fn nested_implicit_namespace_package() -> Result<()> {
- let tempdir = TempDir::new()?;
- let root = ChildPath::new(tempdir.path());
-
- root.child("foo").child("__init__.py").touch()?;
- root.child("foo")
- .child("bar")
- .child("baz")
- .child("__init__.py")
- .touch()?;
- root.child("foo")
- .child("bar")
- .child("baz")
- .child("bop.py")
- .touch()?;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--select")
- .arg("INP")
- .current_dir(&tempdir)
- , @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- ");
-
- insta::with_settings!({filters => vec![(r"\\", "/")]}, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--select")
- .arg("INP")
- .arg("--preview")
- .current_dir(&tempdir)
- , @r"
- success: false
- exit_code: 1
- ----- stdout -----
- foo/bar/baz/__init__.py:1:1: INP001 File `foo/bar/baz/__init__.py` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `foo/bar`.
- Found 1 error.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-#[test]
-fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.flake8-import-conventions.aliases]
-"module.name" = "invalid.alias"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- , @r#"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Failed to load configuration `[TMP]/ruff.toml`
- Cause: Failed to parse [TMP]/ruff.toml
- Cause: TOML parse error at line 3, column 17
- |
- 3 | "module.name" = "invalid.alias"
- | ^^^^^^^^^^^^^^^
- invalid value: string "invalid.alias", expected a Python identifier
- "#);});
- Ok(())
-}
-
-#[test]
-fn flake8_import_convention_invalid_aliases_config_extend_alias_name() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.flake8-import-conventions.extend-aliases]
-"module.name" = "__debug__"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- , @r#"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Failed to load configuration `[TMP]/ruff.toml`
- Cause: Failed to parse [TMP]/ruff.toml
- Cause: TOML parse error at line 3, column 17
- |
- 3 | "module.name" = "__debug__"
- | ^^^^^^^^^^^
- invalid value: string "__debug__", expected an assignable Python identifier
- "#);});
- Ok(())
-}
-
-#[test]
-fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
- let tempdir = TempDir::new()?;
- let ruff_toml = tempdir.path().join("ruff.toml");
- fs::write(
- &ruff_toml,
- r#"
-[lint.flake8-import-conventions.aliases]
-"module..invalid" = "alias"
-"#,
- )?;
-
- insta::with_settings!({
- filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(&ruff_toml)
- , @r#"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- ruff failed
- Cause: Failed to load configuration `[TMP]/ruff.toml`
- Cause: Failed to parse [TMP]/ruff.toml
- Cause: TOML parse error at line 3, column 1
- |
- 3 | "module..invalid" = "alias"
- | ^^^^^^^^^^^^^^^^^
- invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods
- "#);});
- Ok(())
-}
-
-#[test]
-fn flake8_import_convention_unused_aliased_import() {
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(r#"lint.isort.required-imports = ["import pandas"]"#)
- .args(["--select", "I002,ICN001,F401"])
- .args(["--stdin-filename", "test.py"])
- .arg("--unsafe-fixes")
- .arg("--fix")
- .arg("-")
- .pass_stdin("1")
- );
-}
-
-#[test]
-fn flake8_import_convention_unused_aliased_import_no_conflict() {
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#)
- .args(["--select", "I002,ICN001,F401"])
- .args(["--stdin-filename", "test.py"])
- .arg("--unsafe-fixes")
- .arg("--fix")
- .arg("-")
- .pass_stdin("1")
- );
-}
-
-// See: https://github.com/astral-sh/ruff/issues/16177
-#[test]
-fn flake8_pyi_redundant_none_literal() {
- let snippet = r#"
-from typing import Literal
-
-# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
-# but not both, as if both were autofixed it would result in `None | None`,
-# which leads to a `TypeError` at runtime.
-a: Literal[None,] | Literal[None,]
-b: Literal[None] | Literal[None]
-c: Literal[None] | Literal[None,]
-d: Literal[None,] | Literal[None]
-"#;
-
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--select", "PYI061"])
- .args(["--stdin-filename", "test.py"])
- .arg("--preview")
- .arg("--diff")
- .arg("-")
- .pass_stdin(snippet), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- --- test.py
- +++ test.py
- @@ -4,7 +4,7 @@
- # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
- # but not both, as if both were autofixed it would result in `None | None`,
- # which leads to a `TypeError` at runtime.
- -a: Literal[None,] | Literal[None,]
- -b: Literal[None] | Literal[None]
- -c: Literal[None] | Literal[None,]
- -d: Literal[None,] | Literal[None]
- +a: None | Literal[None,]
- +b: None | Literal[None]
- +c: None | Literal[None,]
- +d: None | Literal[None]
-
-
- ----- stderr -----
- Would fix 4 errors.
- ");
-}
-
-/// Test that private, old-style `TypeVar` generics
-/// 1. Get replaced with PEP 695 type parameters (UP046, UP047)
-/// 2. Get renamed to remove leading underscores (UP049)
-/// 3. Emit a warning that the standalone type variable is now unused (PYI018)
-/// 4. Remove the now-unused `Generic` import
-#[test]
-fn pep695_generic_rename() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--select", "F401,PYI018,UP046,UP047,UP049"])
- .args(["--stdin-filename", "test.py"])
- .arg("--unsafe-fixes")
- .arg("--fix")
- .arg("--preview")
- .arg("--target-version=py312")
- .arg("-")
- .pass_stdin(
- r#"
-from typing import Generic, TypeVar
-_T = TypeVar("_T")
-
-class OldStyle(Generic[_T]):
- var: _T
-
-def func(t: _T) -> _T:
- x: _T
- return x
-"#
- ),
- @r"
- success: true
- exit_code: 0
- ----- stdout -----
-
-
- class OldStyle[T]:
- var: T
-
- def func[T](t: T) -> T:
- x: T
- return x
-
- ----- stderr -----
- Found 7 errors (7 fixed, 0 remaining).
- "
- );
-}
-
-/// Test that we do not rename two different type parameters to the same name
-/// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would
-/// introduce invalid syntax)
-#[test]
-fn type_parameter_rename_isolation() {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--select", "UP049"])
- .args(["--stdin-filename", "test.py"])
- .arg("--unsafe-fixes")
- .arg("--fix")
- .arg("--preview")
- .arg("--target-version=py312")
- .arg("-")
- .pass_stdin(
- r#"
-class Foo[_T, __T]:
- pass
-"#
- ),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
-
- class Foo[T, __T]:
- pass
-
- ----- stderr -----
- test.py:2:14: UP049 Generic class uses private type parameters
- Found 2 errors (1 fixed, 1 remaining).
- "
- );
-}
-
-/// construct a directory tree with this structure:
-/// .
-/// ├── abc
-/// │ └── __init__.py
-/// ├── collections
-/// │ ├── __init__.py
-/// │ ├── abc
-/// │ │ └── __init__.py
-/// │ └── foobar
-/// │ └── __init__.py
-/// ├── foobar
-/// │ ├── __init__.py
-/// │ ├── abc
-/// │ │ └── __init__.py
-/// │ └── collections
-/// │ ├── __init__.py
-/// │ ├── abc
-/// │ │ └── __init__.py
-/// │ └── foobar
-/// │ └── __init__.py
-/// ├── ruff.toml
-/// └── urlparse
-/// └── __init__.py
-fn create_a005_module_structure(tempdir: &TempDir) -> Result<()> {
- fn create_module(path: &Path) -> Result<()> {
- fs::create_dir(path)?;
- fs::File::create(path.join("__init__.py"))?;
- Ok(())
- }
-
- let foobar = tempdir.path().join("foobar");
- create_module(&foobar)?;
- for base in [&tempdir.path().into(), &foobar] {
- for dir in ["abc", "collections"] {
- create_module(&base.join(dir))?;
- }
- create_module(&base.join("collections").join("abc"))?;
- create_module(&base.join("collections").join("foobar"))?;
- }
- create_module(&tempdir.path().join("urlparse"))?;
- // also create a ruff.toml to mark the project root
- fs::File::create(tempdir.path().join("ruff.toml"))?;
-
- Ok(())
-}
-
-/// Test A005 with `strict-checking = true`
-#[test]
-fn a005_module_shadowing_strict() -> Result<()> {
- let tempdir = TempDir::new()?;
- create_a005_module_structure(&tempdir)?;
-
- insta::with_settings!({
- filters => vec![(r"\\", "/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(r#"lint.flake8-builtins.strict-checking = true"#)
- .args(["--select", "A005"])
- .current_dir(tempdir.path()),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
- collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
- foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- Found 6 errors.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Test A005 with `strict-checking = false`
-#[test]
-fn a005_module_shadowing_non_strict() -> Result<()> {
- let tempdir = TempDir::new()?;
- create_a005_module_structure(&tempdir)?;
-
- insta::with_settings!({
- filters => vec![(r"\\", "/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--config")
- .arg(r#"lint.flake8-builtins.strict-checking = false"#)
- .args(["--select", "A005"])
- .current_dir(tempdir.path()),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
- Found 2 errors.
-
- ----- stderr -----
- ");
-
- });
-
- Ok(())
-}
-
-/// Test A005 with `strict-checking` unset
-///
-/// This should match the non-strict version directly above
-#[test]
-fn a005_module_shadowing_strict_default() -> Result<()> {
- let tempdir = TempDir::new()?;
- create_a005_module_structure(&tempdir)?;
-
- insta::with_settings!({
- filters => vec![(r"\\", "/")]
- }, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--select", "A005"])
- .current_dir(tempdir.path()),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
- collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
- Found 2 errors.
-
- ----- stderr -----
- ");
- });
- Ok(())
-}
-
-/// Test that the linter respects per-file-target-version.
-#[test]
-fn per_file_target_version_linter() {
- // without per-file-target-version, there should be one UP046 error
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--target-version", "py312"])
- .args(["--select", "UP046"]) // only triggers on 3.12+
- .args(["--stdin-filename", "test.py"])
- .arg("--preview")
- .arg("-")
- .pass_stdin(r#"
-from typing import Generic, TypeVar
-
-T = TypeVar("T")
-
-class A(Generic[T]):
- var: T
-"#),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters
- Found 1 error.
- No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
-
- ----- stderr -----
- "
- );
-
- // with per-file-target-version, there should be no errors because the new generic syntax is
- // unavailable
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--target-version", "py312"])
- .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
- .args(["--select", "UP046"]) // only triggers on 3.12+
- .args(["--stdin-filename", "test.py"])
- .arg("--preview")
- .arg("-")
- .pass_stdin(r#"
-from typing import Generic, TypeVar
-
-T = TypeVar("T")
-
-class A(Generic[T]):
- var: T
-"#),
- @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- "
- );
-}
-
-#[test]
-fn walrus_before_py38() {
- // ok
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--stdin-filename", "test.py"])
- .arg("--target-version=py38")
- .arg("-")
- .pass_stdin(r#"(x := 1)"#),
- @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- "
- );
-
- // not ok on 3.7 with preview
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--stdin-filename", "test.py"])
- .arg("--target-version=py37")
- .arg("--preview")
- .arg("-")
- .pass_stdin(r#"(x := 1)"#),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:1:2: SyntaxError: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
- Found 1 error.
-
- ----- stderr -----
- "
- );
-}
-
-#[test]
-fn match_before_py310() {
- // ok on 3.10
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--stdin-filename", "test.py"])
- .arg("--target-version=py310")
- .arg("-")
- .pass_stdin(
- r#"
-match 2:
- case 1:
- print("it's one")
-"#
- ),
- @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- "
- );
-
- // ok on 3.9 without preview
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--stdin-filename", "test.py"])
- .arg("--target-version=py39")
- .arg("-")
- .pass_stdin(
- r#"
-match 2:
- case 1:
- print("it's one")
-"#
- ),
- @r###"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
- Found 1 error.
-
- ----- stderr -----
- "###
- );
-
- // syntax error on 3.9 with preview
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--stdin-filename", "test.py"])
- .arg("--target-version=py39")
- .arg("--preview")
- .arg("-")
- .pass_stdin(
- r#"
-match 2:
- case 1:
- print("it's one")
-"#
- ),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
- Found 1 error.
-
- ----- stderr -----
- "
- );
-}
-
-/// Regression test for
-#[test]
-fn cache_syntax_errors() -> Result<()> {
- let tempdir = TempDir::new()?;
- fs::write(tempdir.path().join("main.py"), "match 2:\n case 1: ...")?;
-
- let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
- // inline STDIN_BASE_OPTIONS to remove --no-cache
- cmd.args(["check", "--output-format", "concise"])
- .arg("--target-version=py39")
- .arg("--preview")
- .arg("--quiet") // suppress `debug build without --no-cache` warnings
- .current_dir(&tempdir);
-
- assert_cmd_snapshot!(
- cmd,
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.py:1:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
-
- ----- stderr -----
- "
- );
-
- // this should *not* be cached, like normal parse errors
- assert_cmd_snapshot!(
- cmd,
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.py:1:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
-
- ----- stderr -----
- "
- );
-
- Ok(())
-}
-
-/// Regression test for with very helpful
-/// reproduction repo here:
-#[test]
-fn cookiecutter_globbing() -> Result<()> {
- // This is a simplified directory structure from the repo linked above. The essence of the
- // problem is this `{{cookiecutter.repo_name}}` directory containing a config file with a glob.
- // The absolute path of the glob contains the glob metacharacters `{{` and `}}` even though the
- // user's glob does not.
- let tempdir = TempDir::new()?;
- let cookiecutter = tempdir.path().join("{{cookiecutter.repo_name}}");
- let cookiecutter_toml = cookiecutter.join("pyproject.toml");
- let tests = cookiecutter.join("tests");
- fs::create_dir_all(&tests)?;
- fs::write(
- &cookiecutter_toml,
- r#"tool.ruff.lint.per-file-ignores = { "tests/*" = ["F811"] }"#,
- )?;
- // F811 example from the docs to ensure the glob still works
- let maintest = tests.join("maintest.py");
- fs::write(maintest, "import foo\nimport bar\nimport foo")?;
-
- insta::with_settings!({filters => vec![(r"\\", "/")]}, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--select=F811")
- .current_dir(tempdir.path()), @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- ");
- });
-
- // after removing the config file with the ignore, F811 applies, so the glob worked above
- fs::remove_file(cookiecutter_toml)?;
-
- insta::with_settings!({filters => vec![(r"\\", "/")]}, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .arg("--select=F811")
- .current_dir(tempdir.path()), @r"
- success: false
- exit_code: 1
- ----- stdout -----
- {{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1
- Found 1 error.
- [*] 1 fixable with the `--fix` option.
-
- ----- stderr -----
- ");
- });
-
- Ok(())
-}
-
-/// Like the test above but exercises the non-absolute path case in `PerFile::new`
-#[test]
-fn cookiecutter_globbing_no_project_root() -> Result<()> {
- let tempdir = TempDir::new()?;
- let tempdir = tempdir.path().join("{{cookiecutter.repo_name}}");
- fs::create_dir(&tempdir)?;
-
- insta::with_settings!({filters => vec![(r"\\", "/")]}, {
- assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
- .current_dir(&tempdir)
- .args(STDIN_BASE_OPTIONS)
- .args(["--extend-per-file-ignores", "generated.py:Q"]), @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- warning: No Python files found under the given path(s)
- ");
- });
-
- Ok(())
-}
-
-/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the
-/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise
-/// disabling all AST-based rules).
-#[test]
-fn semantic_syntax_errors() -> Result<()> {
- let tempdir = TempDir::new()?;
- let contents = "[(x := 1) for x in foo]";
- fs::write(tempdir.path().join("main.py"), contents)?;
-
- let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
- // inline STDIN_BASE_OPTIONS to remove --no-cache
- cmd.args(["check", "--output-format", "concise"])
- .arg("--preview")
- .arg("--quiet") // suppress `debug build without --no-cache` warnings
- .current_dir(&tempdir);
-
- assert_cmd_snapshot!(
- cmd,
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
- main.py:1:20: F821 Undefined name `foo`
-
- ----- stderr -----
- "
- );
-
- // this should *not* be cached, like normal parse errors
- assert_cmd_snapshot!(
- cmd,
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
- main.py:1:20: F821 Undefined name `foo`
-
- ----- stderr -----
- "
- );
-
- // ensure semantic errors are caught even without AST-based rules selected
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "lint.select = []"])
- .arg("--preview")
- .arg("-")
- .pass_stdin(contents),
- @r"
- success: false
- exit_code: 1
- ----- stdout -----
- -:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
- Found 1 error.
-
- ----- stderr -----
- "
- );
-
- Ok(())
-}
-
-/// Regression test for .
-///
-/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would
-/// try to import `Self` from `typing_extensions`
-#[test]
-fn combine_typing_extensions_config() {
- let contents = "
-from typing import TypeVar
-T = TypeVar('T')
-class Foo:
- def f(self: T) -> T: ...
-";
- assert_cmd_snapshot!(
- Command::new(get_cargo_bin(BIN_NAME))
- .args(STDIN_BASE_OPTIONS)
- .args(["--config", "lint.typing-extensions = false"])
- .arg("--select=PYI019")
- .arg("--target-version=py39")
- .arg("-")
- .pass_stdin(contents),
- @r"
- success: true
- exit_code: 0
- ----- stdout -----
- All checks passed!
-
- ----- stderr -----
- "
- );
-}
diff --git a/crates/ruff/tests/snapshots/format__output_format_azure.snap b/crates/ruff/tests/snapshots/format__output_format_azure.snap
new file mode 100644
index 0000000000..f27a327727
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_azure.snap
@@ -0,0 +1,19 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - azure
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=1;code=unformatted;]File would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_concise.snap b/crates/ruff/tests/snapshots/format__output_format_concise.snap
new file mode 100644
index 0000000000..8a24633768
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_concise.snap
@@ -0,0 +1,20 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - concise
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:1:1: unformatted: File would be reformatted
+1 file would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_full.snap b/crates/ruff/tests/snapshots/format__output_format_full.snap
new file mode 100644
index 0000000000..30eb3a0a51
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_full.snap
@@ -0,0 +1,26 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - full
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+unformatted: File would be reformatted
+ --> input.py:1:1
+ -
+1 | from test import say_hy
+2 |
+3 | if __name__ == "__main__":
+
+1 file would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_github.snap b/crates/ruff/tests/snapshots/format__output_format_github.snap
new file mode 100644
index 0000000000..e279ad2b87
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_github.snap
@@ -0,0 +1,19 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - github
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+::error title=Ruff (unformatted),file=[TMP]/input.py,line=1,col=1,endLine=2,endColumn=1::input.py:1:1: unformatted: File would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_gitlab.snap b/crates/ruff/tests/snapshots/format__output_format_gitlab.snap
new file mode 100644
index 0000000000..fbf68871d3
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_gitlab.snap
@@ -0,0 +1,38 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - gitlab
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+[
+ {
+ "check_name": "unformatted",
+ "description": "unformatted: File would be reformatted",
+ "severity": "major",
+ "fingerprint": "d868d7da11a65fcf",
+ "location": {
+ "path": "input.py",
+ "positions": {
+ "begin": {
+ "line": 1,
+ "column": 1
+ },
+ "end": {
+ "line": 2,
+ "column": 1
+ }
+ }
+ }
+ }
+]
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_grouped.snap b/crates/ruff/tests/snapshots/format__output_format_grouped.snap
new file mode 100644
index 0000000000..bd9ec0e7e4
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_grouped.snap
@@ -0,0 +1,21 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - grouped
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:
+ 1:1 unformatted: File would be reformatted
+
+1 file would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_json-lines.snap b/crates/ruff/tests/snapshots/format__output_format_json-lines.snap
new file mode 100644
index 0000000000..dadde72435
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_json-lines.snap
@@ -0,0 +1,19 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - json-lines
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+{"cell":null,"code":"unformatted","end_location":{"column":1,"row":2},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":null},"location":{"column":1,"row":1},"message":"File would be reformatted","noqa_row":null,"url":null}
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_json.snap b/crates/ruff/tests/snapshots/format__output_format_json.snap
new file mode 100644
index 0000000000..1bbff05aec
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_json.snap
@@ -0,0 +1,52 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - json
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+[
+ {
+ "cell": null,
+ "code": "unformatted",
+ "end_location": {
+ "column": 1,
+ "row": 2
+ },
+ "filename": "[TMP]/input.py",
+ "fix": {
+ "applicability": "safe",
+ "edits": [
+ {
+ "content": "",
+ "end_location": {
+ "column": 1,
+ "row": 2
+ },
+ "location": {
+ "column": 1,
+ "row": 1
+ }
+ }
+ ],
+ "message": null
+ },
+ "location": {
+ "column": 1,
+ "row": 1
+ },
+ "message": "File would be reformatted",
+ "noqa_row": null,
+ "url": null
+ }
+]
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_junit.snap b/crates/ruff/tests/snapshots/format__output_format_junit.snap
new file mode 100644
index 0000000000..518c836729
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_junit.snap
@@ -0,0 +1,26 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - junit
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+
+
+
+
+ line 1, col 1, File would be reformatted
+
+
+
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_pylint.snap b/crates/ruff/tests/snapshots/format__output_format_pylint.snap
new file mode 100644
index 0000000000..7d5f80fed4
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_pylint.snap
@@ -0,0 +1,18 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - pylint
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+input.py:1: [unformatted] File would be reformatted
+
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_rdjson.snap b/crates/ruff/tests/snapshots/format__output_format_rdjson.snap
new file mode 100644
index 0000000000..dcdb5bda8c
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_rdjson.snap
@@ -0,0 +1,60 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - rdjson
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+{
+ "diagnostics": [
+ {
+ "code": {
+ "value": "unformatted"
+ },
+ "location": {
+ "path": "[TMP]/input.py",
+ "range": {
+ "end": {
+ "column": 1,
+ "line": 2
+ },
+ "start": {
+ "column": 1,
+ "line": 1
+ }
+ }
+ },
+ "message": "File would be reformatted",
+ "suggestions": [
+ {
+ "range": {
+ "end": {
+ "column": 1,
+ "line": 2
+ },
+ "start": {
+ "column": 1,
+ "line": 1
+ }
+ },
+ "text": ""
+ }
+ ]
+ }
+ ],
+ "severity": "WARNING",
+ "source": {
+ "name": "ruff",
+ "url": "https://docs.astral.sh/ruff"
+ }
+}
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/format__output_format_sarif.snap b/crates/ruff/tests/snapshots/format__output_format_sarif.snap
new file mode 100644
index 0000000000..1f31ab0374
--- /dev/null
+++ b/crates/ruff/tests/snapshots/format__output_format_sarif.snap
@@ -0,0 +1,81 @@
+---
+source: crates/ruff/tests/format.rs
+info:
+ program: ruff
+ args:
+ - format
+ - "--no-cache"
+ - "--output-format"
+ - sarif
+ - "--preview"
+ - "--check"
+ - input.py
+---
+success: false
+exit_code: 1
+----- stdout -----
+{
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+ "runs": [
+ {
+ "results": [
+ {
+ "fixes": [
+ {
+ "artifactChanges": [
+ {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "replacements": [
+ {
+ "deletedRegion": {
+ "endColumn": 1,
+ "endLine": 2,
+ "startColumn": 1,
+ "startLine": 1
+ }
+ }
+ ]
+ }
+ ],
+ "description": {
+ "text": null
+ }
+ }
+ ],
+ "level": "error",
+ "locations": [
+ {
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "[TMP]/input.py"
+ },
+ "region": {
+ "endColumn": 1,
+ "endLine": 2,
+ "startColumn": 1,
+ "startLine": 1
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "File would be reformatted"
+ },
+ "ruleId": "unformatted"
+ }
+ ],
+ "tool": {
+ "driver": {
+ "informationUri": "https://github.com/astral-sh/ruff",
+ "name": "ruff",
+ "rules": [],
+ "version": "[VERSION]"
+ }
+ }
+ }
+ ],
+ "version": "2.1.0"
+}
+----- stderr -----
diff --git a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap
index a861fe395c..9213ccb0d7 100644
--- a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap
+++ b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap
@@ -44,6 +44,43 @@ import some_module
__all__ = ["some_module"]
```
+## Preview
+When [preview] is enabled (and certain simplifying assumptions
+are met), we analyze all import statements for a given module
+when determining whether an import is used, rather than simply
+the last of these statements. This can result in both different and
+more import statements being marked as unused.
+
+For example, if a module consists of
+
+```python
+import a
+import a.b
+```
+
+then both statements are marked as unused under [preview], whereas
+only the second is marked as unused under stable behavior.
+
+As another example, if a module consists of
+
+```python
+import a.b
+import a
+
+a.b.foo()
+```
+
+then a diagnostic will only be emitted for the first line under [preview],
+whereas a diagnostic would only be emitted for the second line under
+stable behavior.
+
+Note that this behavior is somewhat subjective and is designed
+to conform to the developer's intuition rather than Python's actual
+execution. To wit, the statement `import a.b` automatically executes
+`import a`, so in some sense `import a` is _always_ redundant
+in the presence of `import a.b`.
+
+
## Fix safety
Fixes to remove unused imports are safe, except in `__init__.py` files.
@@ -55,6 +92,10 @@ either a redundant alias or, if already present in the file, an `__all__` entry.
to remove third-party and standard library imports -- the fix is unsafe because the module's
interface changes.
+See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
+for more details on how Ruff
+determines whether an import is first or third-party.
+
## Example
```python
@@ -83,11 +124,6 @@ else:
print("numpy is not installed")
```
-## Preview
-When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
-the criterion for determining whether an import is first-party
-is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
-
## Options
- `lint.ignore-init-module-imports`
- `lint.pyflakes.allowed-unused-imports`
@@ -95,6 +131,8 @@ is stricter, which could affect the suggested fix. See [this FAQ section](https:
## References
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
-- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
+- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
+
+[preview]: https://docs.astral.sh/ruff/preview/
----- stderr -----
diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap
index 37e8eae6bc..28a6607816 100644
--- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap
+++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap
@@ -371,6 +371,7 @@ linter.pylint.max_branches = 12
linter.pylint.max_statements = 50
linter.pylint.max_public_methods = 20
linter.pylint.max_locals = 15
+linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
@@ -392,7 +393,7 @@ formatter.docstring_code_line_width = dynamic
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.7
-analyze.detect_string_imports = false
+analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
diff --git a/crates/ruff/tests/version.rs b/crates/ruff/tests/version.rs
index 6dbf7ba97d..2881b55f7f 100644
--- a/crates/ruff/tests/version.rs
+++ b/crates/ruff/tests/version.rs
@@ -60,7 +60,7 @@ fn config_option_ignored_but_validated() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
- .args(["--config", "foo = bar"]), @r#"
+ .args(["--config", "foo = bar"]), @r"
success: false
exit_code: 2
----- stdout -----
@@ -77,12 +77,11 @@ fn config_option_ignored_but_validated() {
TOML parse error at line 1, column 7
|
1 | foo = bar
- | ^
- invalid string
- expected `"`, `'`
+ | ^^^
+ string values must be quoted, expected literal string
For more information, try '--help'.
- "#
+ "
);
});
}
diff --git a/crates/ruff_annotate_snippets/src/lib.rs b/crates/ruff_annotate_snippets/src/lib.rs
index f8d0e1e5c6..e623489f97 100644
--- a/crates/ruff_annotate_snippets/src/lib.rs
+++ b/crates/ruff_annotate_snippets/src/lib.rs
@@ -1,3 +1,5 @@
+#![expect(clippy::needless_doctest_main)]
+
//! A library for formatting of text or programming code snippets.
//!
//! It's primary purpose is to build an ASCII-graphical representation of the snippet
diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs
index c445c6c898..fb88a26223 100644
--- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs
+++ b/crates/ruff_annotate_snippets/src/renderer/display_list.rs
@@ -56,6 +56,7 @@ pub(crate) struct DisplayList<'a> {
pub(crate) stylesheet: &'a Stylesheet,
pub(crate) anonymized_line_numbers: bool,
pub(crate) cut_indicator: &'static str,
+ pub(crate) lineno_offset: usize,
}
impl PartialEq for DisplayList<'_> {
@@ -81,13 +82,14 @@ impl Display for DisplayList<'_> {
_ => max,
})
});
- let lineno_width = if lineno_width == 0 {
- lineno_width
- } else if self.anonymized_line_numbers {
- ANONYMIZED_LINE_NUM.len()
- } else {
- ((lineno_width as f64).log10().floor() as usize) + 1
- };
+ let lineno_width = self.lineno_offset
+ + if lineno_width == 0 {
+ lineno_width
+ } else if self.anonymized_line_numbers {
+ ANONYMIZED_LINE_NUM.len()
+ } else {
+ ((lineno_width as f64).log10().floor() as usize) + 1
+ };
let multiline_depth = self.body.iter().fold(0, |max, set| {
set.display_lines.iter().fold(max, |max2, line| match line {
@@ -124,6 +126,7 @@ impl<'a> DisplayList<'a> {
term_width: usize,
cut_indicator: &'static str,
) -> DisplayList<'a> {
+ let lineno_offset = message.lineno_offset;
let body = format_message(
message,
term_width,
@@ -137,6 +140,7 @@ impl<'a> DisplayList<'a> {
stylesheet,
anonymized_line_numbers,
cut_indicator,
+ lineno_offset,
}
}
@@ -193,9 +197,14 @@ impl DisplaySet<'_> {
stylesheet: &Stylesheet,
buffer: &mut StyledBuffer,
) -> fmt::Result {
+ let hide_severity = annotation.annotation_type.is_none();
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
let formatted_len = if let Some(id) = &annotation.id {
- 2 + id.len() + annotation_type_len(&annotation.annotation_type)
+ if hide_severity {
+ id.len()
+ } else {
+ 2 + id.len() + annotation_type_len(&annotation.annotation_type)
+ }
} else {
annotation_type_len(&annotation.annotation_type)
};
@@ -209,18 +218,66 @@ impl DisplaySet<'_> {
if formatted_len == 0 {
self.format_label(line_offset, &annotation.label, stylesheet, buffer)
} else {
- let id = match &annotation.id {
- Some(id) => format!("[{id}]"),
- None => String::new(),
- };
- buffer.append(
- line_offset,
- &format!("{}{}", annotation_type_str(&annotation.annotation_type), id),
- *color,
- );
+ // TODO(brent) All of this complicated checking of `hide_severity` should be reverted
+ // once we have real severities in Ruff. This code is trying to account for two
+ // different cases:
+ //
+ // - main diagnostic message
+ // - subdiagnostic message
+ //
+ // In the first case, signaled by `hide_severity = true`, we want to print the ID (the
+ // noqa code for a ruff lint diagnostic, e.g. `F401`, or `invalid-syntax` for a syntax
+ // error) without brackets. Instead, for subdiagnostics, we actually want to print the
+ // severity (usually `help`) regardless of the `hide_severity` setting. This is signaled
+ // by an ID of `None`.
+ //
+ // With real severities these should be reported more like in ty:
+ //
+ // ```
+ // error[F401]: `math` imported but unused
+ // error[invalid-syntax]: Cannot use `match` statement on Python 3.9...
+ // ```
+ //
+ // instead of the current versions intended to mimic the old Ruff output format:
+ //
+ // ```
+ // F401 `math` imported but unused
+ // invalid-syntax: Cannot use `match` statement on Python 3.9...
+ // ```
+ //
+ // Note that the `invalid-syntax` colon is added manually in `ruff_db`, not here. We
+ // could eventually add a colon to Ruff lint diagnostics (`F401:`) and then make the
+ // colon below unconditional again.
+ //
+ // This also applies to the hard-coded `stylesheet.error()` styling of the
+ // hidden-severity `id`. This should just be `*color` again later, but for now we don't
+ // want an unformatted `id`, which is what `get_annotation_style` returns for
+ // `DisplayAnnotationType::None`.
+ let annotation_type = annotation_type_str(&annotation.annotation_type);
+ if let Some(id) = annotation.id {
+ if hide_severity {
+ buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
+ } else {
+ buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
+ }
+ } else {
+ buffer.append(line_offset, annotation_type, *color);
+ }
+
+ if annotation.is_fixable {
+ buffer.append(line_offset, "[", stylesheet.none);
+ buffer.append(line_offset, "*", stylesheet.help);
+ buffer.append(line_offset, "]", stylesheet.none);
+ // In the hide-severity case, we need a space instead of the colon and space below.
+ if hide_severity {
+ buffer.append(line_offset, " ", stylesheet.none);
+ }
+ }
if !is_annotation_empty(annotation) {
- buffer.append(line_offset, ": ", stylesheet.none);
+ if annotation.id.is_none() || !hide_severity {
+ buffer.append(line_offset, ": ", stylesheet.none);
+ }
self.format_label(line_offset, &annotation.label, stylesheet, buffer)?;
}
Ok(())
@@ -249,11 +306,15 @@ impl DisplaySet<'_> {
let lineno_color = stylesheet.line_no();
buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color);
buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none);
- if let Some((col, row)) = pos {
- buffer.append(line_offset, ":", stylesheet.none);
- buffer.append(line_offset, col.to_string().as_str(), stylesheet.none);
+ if let Some(Position { row, col, cell }) = pos {
+ if let Some(cell) = cell {
+ buffer.append(line_offset, ":", stylesheet.none);
+ buffer.append(line_offset, &format!("cell {cell}"), stylesheet.none);
+ }
buffer.append(line_offset, ":", stylesheet.none);
buffer.append(line_offset, row.to_string().as_str(), stylesheet.none);
+ buffer.append(line_offset, ":", stylesheet.none);
+ buffer.append(line_offset, col.to_string().as_str(), stylesheet.none);
}
Ok(())
}
@@ -768,6 +829,7 @@ pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<&'a str>,
pub(crate) label: Vec>,
+ pub(crate) is_fixable: bool,
}
/// A single line used in `DisplayList`.
@@ -833,6 +895,13 @@ impl DisplaySourceAnnotation<'_> {
}
}
+#[derive(Debug, PartialEq)]
+pub(crate) struct Position {
+ row: usize,
+ col: usize,
+ cell: Option,
+}
+
/// Raw line - a line which does not have the `lineno` part and is not considered
/// a part of the snippet.
#[derive(Debug, PartialEq)]
@@ -841,7 +910,7 @@ pub(crate) enum DisplayRawLine<'a> {
/// slice in the project structure.
Origin {
path: &'a str,
- pos: Option<(usize, usize)>,
+ pos: Option,
header_type: DisplayHeaderType,
},
@@ -920,6 +989,13 @@ pub(crate) enum DisplayAnnotationType {
Help,
}
+impl DisplayAnnotationType {
+ #[inline]
+ const fn is_none(&self) -> bool {
+ matches!(self, Self::None)
+ }
+}
+
impl From for DisplayAnnotationType {
fn from(at: snippet::Level) -> Self {
match at {
@@ -1015,11 +1091,13 @@ fn format_message<'m>(
title,
footer,
snippets,
+ is_fixable,
+ lineno_offset: _,
} = message;
let mut sets = vec![];
let body = if !snippets.is_empty() || primary {
- vec![format_title(level, id, title)]
+ vec![format_title(level, id, title, is_fixable)]
} else {
format_footer(level, id, title)
};
@@ -1060,12 +1138,18 @@ fn format_message<'m>(
sets
}
-fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> {
+fn format_title<'a>(
+ level: crate::Level,
+ id: Option<&'a str>,
+ label: &'a str,
+ is_fixable: bool,
+) -> DisplayLine<'a> {
DisplayLine::Raw(DisplayRawLine::Annotation {
annotation: Annotation {
annotation_type: DisplayAnnotationType::from(level),
id,
label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)),
+ is_fixable,
},
source_aligned: false,
continuation: false,
@@ -1084,6 +1168,7 @@ fn format_footer<'a>(
annotation_type: DisplayAnnotationType::from(level),
id,
label: format_label(Some(line), None),
+ is_fixable: false,
},
source_aligned: true,
continuation: i != 0,
@@ -1118,6 +1203,28 @@ fn format_snippet<'m>(
let main_range = snippet.annotations.first().map(|x| x.range.start);
let origin = snippet.origin;
let need_empty_header = origin.is_some() || is_first;
+
+ let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level);
+ if is_file_level {
+ // TODO(brent) enable this assertion again once we set `is_file_level` for individual rules.
+ // It's causing too many false positives currently when the default is to make any
+ // annotation with a default range file-level. See
+ // https://github.com/astral-sh/ruff/issues/19688.
+ //
+ // assert!(
+ // snippet.source.is_empty(),
+ // "Non-empty file-level snippet that won't be rendered: {:?}",
+ // snippet.source
+ // );
+ let header = format_header(origin, main_range, &[], is_first, snippet.cell_index);
+ return DisplaySet {
+ display_lines: header.map_or_else(Vec::new, |header| vec![header]),
+ margin: Margin::new(0, 0, 0, 0, term_width, 0),
+ };
+ }
+
+ let cell_index = snippet.cell_index;
+
let mut body = format_body(
snippet,
need_empty_header,
@@ -1126,7 +1233,13 @@ fn format_snippet<'m>(
anonymized_line_numbers,
cut_indicator,
);
- let header = format_header(origin, main_range, &body.display_lines, is_first);
+ let header = format_header(
+ origin,
+ main_range,
+ &body.display_lines,
+ is_first,
+ cell_index,
+ );
if let Some(header) = header {
body.display_lines.insert(0, header);
@@ -1146,6 +1259,7 @@ fn format_header<'a>(
main_range: Option,
body: &[DisplayLine<'_>],
is_first: bool,
+ cell_index: Option,
) -> Option> {
let display_header = if is_first {
DisplayHeaderType::Initial
@@ -1169,20 +1283,31 @@ fn format_header<'a>(
..
} = item
{
- if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) {
+ // At the very end of the `main_range`, report the location as the first character
+ // in the next line instead of falling back to the default location of `1:1`. This
+ // is another divergence from upstream.
+ let end_of_range = range.1 + max(*end_line as usize, 1);
+ if main_range >= range.0 && main_range < end_of_range {
let char_column = text[0..(main_range - range.0).min(text.len())]
.chars()
.count();
col = char_column + 1;
line_offset = lineno.unwrap_or(1);
break;
+ } else if main_range == end_of_range {
+ line_offset = lineno.map_or(1, |line| line + 1);
+ break;
}
}
}
return Some(DisplayLine::Raw(DisplayRawLine::Origin {
path,
- pos: Some((line_offset, col)),
+ pos: Some(Position {
+ row: line_offset,
+ col,
+ cell: cell_index,
+ }),
header_type: display_header,
}));
}
@@ -1472,6 +1597,7 @@ fn format_body<'m>(
annotation_type,
id: None,
label: format_label(annotation.label, None),
+ is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),
@@ -1511,6 +1637,7 @@ fn format_body<'m>(
annotation_type,
id: None,
label: vec![],
+ is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),
@@ -1580,6 +1707,7 @@ fn format_body<'m>(
annotation_type,
id: None,
label: format_label(annotation.label, None),
+ is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),
diff --git a/crates/ruff_annotate_snippets/src/snippet.rs b/crates/ruff_annotate_snippets/src/snippet.rs
index 5830a7b5cd..76d615189d 100644
--- a/crates/ruff_annotate_snippets/src/snippet.rs
+++ b/crates/ruff_annotate_snippets/src/snippet.rs
@@ -22,6 +22,8 @@ pub struct Message<'a> {
pub(crate) title: &'a str,
pub(crate) snippets: Vec>,
pub(crate) footer: Vec>,
+ pub(crate) is_fixable: bool,
+ pub(crate) lineno_offset: usize,
}
impl<'a> Message<'a> {
@@ -49,6 +51,25 @@ impl<'a> Message<'a> {
self.footer.extend(footer);
self
}
+
+ /// Whether or not the diagnostic for this message is fixable.
+ ///
+ /// This is rendered as a `[*]` indicator after the `id` in an annotation header, if the
+ /// annotation also has `Level::None`.
+ pub fn is_fixable(mut self, yes: bool) -> Self {
+ self.is_fixable = yes;
+ self
+ }
+
+ /// Add an offset used for aligning the header sigil (`-->`) with the line number separators.
+ ///
+ /// For normal diagnostics this is computed automatically based on the lines to be rendered.
+ /// This is intended only for use in the formatter, where we don't render a snippet directly but
+ /// still want the header to align with the diff.
+ pub fn lineno_offset(mut self, offset: usize) -> Self {
+ self.lineno_offset = offset;
+ self
+ }
}
/// Structure containing the slice of text to be annotated and
@@ -65,6 +86,10 @@ pub struct Snippet<'a> {
pub(crate) annotations: Vec>,
pub(crate) fold: bool,
+
+ /// The optional cell index in a Jupyter notebook, used for reporting source locations along
+ /// with the ranges on `annotations`.
+ pub(crate) cell_index: Option,
}
impl<'a> Snippet<'a> {
@@ -75,6 +100,7 @@ impl<'a> Snippet<'a> {
source,
annotations: vec![],
fold: false,
+ cell_index: None,
}
}
@@ -103,6 +129,12 @@ impl<'a> Snippet<'a> {
self.fold = fold;
self
}
+
+ /// Attach a Jupyter notebook cell index.
+ pub fn cell_index(mut self, index: Option) -> Self {
+ self.cell_index = index;
+ self
+ }
}
/// An annotation for a [`Snippet`].
@@ -114,6 +146,7 @@ pub struct Annotation<'a> {
pub(crate) range: Range,
pub(crate) label: Option<&'a str>,
pub(crate) level: Level,
+ pub(crate) is_file_level: bool,
}
impl<'a> Annotation<'a> {
@@ -121,6 +154,11 @@ impl<'a> Annotation<'a> {
self.label = Some(label);
self
}
+
+ pub fn hide_snippet(mut self, yes: bool) -> Self {
+ self.is_file_level = yes;
+ self
+ }
}
/// Types of annotations.
@@ -145,6 +183,8 @@ impl Level {
title,
snippets: vec![],
footer: vec![],
+ is_fixable: false,
+ lineno_offset: 0,
}
}
@@ -154,6 +194,7 @@ impl Level {
range: span,
label: None,
level: self,
+ is_file_level: false,
}
}
}
diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_eof.svg b/crates/ruff_annotate_snippets/tests/fixtures/color/ann_eof.svg
index bb12aecf29..045b0ef413 100644
--- a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_eof.svg
+++ b/crates/ruff_annotate_snippets/tests/fixtures/color/ann_eof.svg
@@ -1,7 +1,7 @@