Compare commits

..

5 Commits

Author SHA1 Message Date
Jane Lewis
7f9c31720b Apply ruff-vscode settings suggestion 2024-06-28 06:29:32 -07:00
Jane Lewis
6034a4e0ba Apply suggestions
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-06-28 06:27:37 -07:00
Jane Lewis
e7a7da9ae9 Reduce all headings and re-add a single top-level heading 2024-06-28 05:39:12 -07:00
Jane Lewis
2fda0038d8 Remove unavailable link 2024-06-26 15:32:06 -07:00
Jane Lewis
800ff0a693 Update Integrations page to cover ruff server 2024-06-26 15:28:43 -07:00
219 changed files with 5341 additions and 6581 deletions

View File

@@ -1,68 +0,0 @@
# Build and publish a Docker image.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
#
# TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but
# sharing the built image as an artifact between jobs is challenging.
name: "[ruff] Build Docker image"
on:
workflow_call:
inputs:
plan:
required: true
type: string
pull_request:
paths:
- .github/workflows/build-docker.yml
jobs:
docker-publish:
name: Build Docker image (ghcr.io/astral-sh/ruff)
runs-on: ubuntu-latest
environment:
name: release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || 'dry-run' }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,7 +1,3 @@
# Publish the Ruff documentation.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a post-announce
# job within `cargo-dist`.
name: mkdocs
on:
@@ -11,11 +7,8 @@ on:
description: "The commit SHA, tag, or branch to publish. Uses the default branch if not specified."
default: ""
type: string
workflow_call:
inputs:
plan:
required: true
type: string
release:
types: [published]
jobs:
mkdocs:

View File

@@ -1,29 +0,0 @@
# Notify downstream repositories of a new release.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a post-announce
# job within `cargo-dist`.
name: "[ruff] Notify dependents"
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
update-dependents:
name: Notify dependents
runs-on: ubuntu-latest
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'astral-sh',
repo: 'ruff-pre-commit',
workflow_id: 'main.yml',
ref: 'main',
})

View File

@@ -1,16 +1,9 @@
# Publish the Ruff playground.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a post-announce
# job within `cargo-dist`.
name: "[Playground] Release"
on:
workflow_dispatch:
workflow_call:
inputs:
plan:
required: true
type: string
release:
types: [published]
env:
CARGO_INCREMENTAL: 0

View File

@@ -1,34 +0,0 @@
# Publish a release to PyPI.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job
# within `cargo-dist`.
name: "[ruff] Publish to PyPI"
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
pypi-publish:
name: Upload to PyPI
runs-on: ubuntu-latest
environment:
name: release
permissions:
# For PyPI's trusted publishing.
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true

View File

@@ -1,23 +1,21 @@
# Build ruff on all platforms.
#
# Generates both wheels (for PyPI) and archived binaries (for GitHub releases).
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
name: "Build binaries"
name: "[ruff] Release"
on:
workflow_call:
workflow_dispatch:
inputs:
plan:
required: true
tag:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
default: ""
type: string
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work.
# When we change pyproject.toml, we want to ensure that the maturin builds still work
- pyproject.toml
# And when we change this workflow itself...
- .github/workflows/build-binaries.yml
- .github/workflows/release.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -25,7 +23,6 @@ concurrency:
env:
PACKAGE_NAME: ruff
MODULE_NAME: ruff
PYTHON_VERSION: "3.11"
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -34,12 +31,11 @@ env:
jobs:
sdist:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -53,8 +49,8 @@ jobs:
- name: "Test sdist"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
ruff --help
python -m ruff --help
- name: "Upload sdist"
uses: actions/upload-artifact@v4
with:
@@ -62,12 +58,11 @@ jobs:
path: dist
macos-x86_64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-12
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -79,6 +74,11 @@ jobs:
with:
target: x86_64
args: --release --locked --out dist
- name: "Test wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -86,29 +86,23 @@ jobs:
path: dist
- name: "Archive binary"
run: |
TARGET=x86_64-apple-darwin
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-macos-x86_64
name: binaries-macos-x86_64
path: |
*.tar.gz
*.sha256
macos-aarch64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -132,24 +126,18 @@ jobs:
path: dist
- name: "Archive binary"
run: |
TARGET=aarch64-apple-darwin
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-aarch64-apple-darwin
name: binaries-aarch64-apple-darwin
path: |
*.tar.gz
*.sha256
windows:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: windows-latest
strategy:
matrix:
@@ -163,7 +151,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -183,8 +171,8 @@ jobs:
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -193,19 +181,18 @@ jobs:
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
name: binaries-${{ matrix.platform.target }}
path: |
*.zip
*.sha256
linux:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -215,7 +202,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -232,36 +219,27 @@ jobs:
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
set -euo pipefail
TARGET=${{ matrix.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.target }}
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
linux-cross:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -283,13 +261,11 @@ jobs:
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: arm-unknown-linux-musleabihf
arch: arm
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -306,8 +282,8 @@ jobs:
if: matrix.platform.arch != 'ppc64'
name: Test wheel
with:
arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }}
distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }}
arch: ${{ matrix.platform.arch }}
distro: ubuntu20.04
githubToken: ${{ github.token }}
install: |
apt-get update
@@ -322,28 +298,19 @@ jobs:
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
set -euo pipefail
TARGET=${{ matrix.platform.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
musllinux:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -353,7 +320,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -376,35 +343,26 @@ jobs:
apk add python3
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
set -euo pipefail
TARGET=${{ matrix.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.target }}
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
musllinux-cross:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -418,7 +376,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -442,29 +400,204 @@ jobs:
run: |
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
set -euo pipefail
TARGET=${{ matrix.platform.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
validate-tag:
name: Validate tag
runs-on: ubuntu-latest
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: main # We checkout the main branch to check for the commit
- name: Check main branch
if: ${{ inputs.sha }}
run: |
# Fetch the main branch since a shallow checkout is used by default
git fetch origin main --unshallow
if ! git branch --contains ${{ inputs.sha }} | grep -E '(^|\s)main$'; then
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check tag consistency
run: |
# Switch to the commit we want to release
git checkout ${{ inputs.sha }}
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
upload-release:
name: Upload to PyPI
runs-on: ubuntu-latest
needs:
- macos-aarch64
- macos-x86_64
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
- validate-tag
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true
tag-release:
name: Tag release
runs-on: ubuntu-latest
needs: upload-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For git tag
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: git tag
run: |
git config user.email "hey@astral.sh"
git config user.name "Ruff Release CI"
git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}"
# If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip
# existing), so we make a non-destructive exit here
git push --tags
publish-release:
name: Publish to GitHub
runs-on: ubuntu-latest
needs: tag-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: binaries-*
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v2
with:
draft: true
files: binaries/*
tag_name: v${{ inputs.tag }}
docker-publish:
# This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
# the tag here also
name: Push Docker image ghcr.io/astral-sh/ruff
runs-on: ubuntu-latest
environment:
name: release
permissions:
# For the docker push
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
# Unlike validate-tag we don't check if the commit is on the main branch, but it seems good enough since we can
# change docker tags
if: ${{ inputs.tag }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.tag != '' }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ inputs.tag || 'dry-run' }}
labels: ${{ steps.meta.outputs.labels }}
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Update dependents
runs-on: ubuntu-latest
needs: publish-release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'astral-sh',
repo: 'ruff-pre-commit',
workflow_id: 'main.yml',
ref: 'main',
})

View File

@@ -1,267 +0,0 @@
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
contents: write
# This task will run whenever you workflow_dispatch with a tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
workflow_dispatch:
inputs:
tag:
description: Release Tag
required: true
default: dry-run
type: string
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: ubuntu-latest
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }}
tag-flag: ${{ inputs.tag && inputs.tag != 'dry-run' && format('--tag={0}', inputs.tag) || '' }}
publishing: ${{ inputs.tag && inputs.tag != 'dry-run' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
custom-build-binaries:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
uses: ./.github/workflows/build-binaries.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
custom-build-docker:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
uses: ./.github/workflows/build-docker.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
permissions:
packages: write
contents: read
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- custom-build-binaries
- custom-build-docker
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- custom-build-binaries
- custom-build-docker
- build-global-artifacts
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
# This is a harmless no-op for GitHub Releases, hosting for that happens in "announce"
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
custom-publish-pypi:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-pypi.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
id-token: write
packages: write
# Create a GitHub Release while uploading all files to it
announce:
needs:
- plan
- host
- custom-publish-pypi
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.plan.outputs.tag }}
name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
artifacts: "artifacts/*"
custom-notify-dependents:
needs:
- plan
- announce
uses: ./.github/workflows/notify-dependents.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
custom-publish-docs:
needs:
- plan
- announce
uses: ./.github/workflows/publish-docs.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
custom-publish-playground:
needs:
- plan
- announce
uses: ./.github/workflows/publish-playground.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

View File

@@ -1,2 +0,0 @@
# Auto-generated by `cargo-dist`.
.github/workflows/release.yml

View File

@@ -1,12 +1,5 @@
# Breaking Changes
## 0.5.0
- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms)
- Selecting `ALL` now excludes deprecated rules
- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring.
- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub.
## 0.3.0
### Ruff 2024.2 style

View File

@@ -1,142 +1,5 @@
# Changelog
## 0.5.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.5.0) for a migration guide and overview of the changes!
### Breaking changes
See also, the "Remapped rules" section which may result in disabled rules.
- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms)
- Selecting `ALL` now excludes deprecated rules
- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring.
- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub.
- The diagnostic ranges for some `flake8-bandit` rules were modified ([#10667](https://github.com/astral-sh/ruff/pull/10667)).
### Deprecations
The following rules are now deprecated:
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`): Syntax errors are now always shown
### Remapped rules
The following rules have been remapped to new rule codes:
- [`blocking-http-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-in-async-function/): `ASYNC100` to `ASYNC210`
- [`open-sleep-or-subprocess-in-async-function`](https://docs.astral.sh/ruff/rules/open-sleep-or-subprocess-in-async-function/): `ASYNC101` split into `ASYNC220`, `ASYNC221`, `ASYNC230`, and `ASYNC251`
- [`blocking-os-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-os-call-in-async-function/): `ASYNC102` has been merged into `ASYNC220` and `ASYNC221`
- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await/): `TRIO100` to `ASYNC100`
- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call/): `TRIO105` to `ASYNC105`
- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout/): `TRIO109` to `ASYNC109`
- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep/): `TRIO110` to `ASYNC110`
- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call/): `TRIO115` to `ASYNC115`
- [`repeated-isinstance-calls`](https://docs.astral.sh/ruff/rules/repeated-isinstance-calls/): `PLR1701` to `SIM101`
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`mutable-fromkeys-value`](https://docs.astral.sh/ruff/rules/mutable-fromkeys-value/) (`RUF024`)
- [`default-factory-kwarg`](https://docs.astral.sh/ruff/rules/default-factory-kwarg/) (`RUF026`)
- [`django-extra`](https://docs.astral.sh/ruff/rules/django-extra/) (`S610`)
- [`manual-dict-comprehension`](https://docs.astral.sh/ruff/rules/manual-dict-comprehension/) (`PERF403`)
- [`print-empty-string`](https://docs.astral.sh/ruff/rules/print-empty-string/) (`FURB105`)
- [`readlines-in-for`](https://docs.astral.sh/ruff/rules/readlines-in-for/) (`FURB129`)
- [`if-expr-min-max`](https://docs.astral.sh/ruff/rules/if-expr-min-max/) (`FURB136`)
- [`bit-count`](https://docs.astral.sh/ruff/rules/bit-count/) (`FURB161`)
- [`redundant-log-base`](https://docs.astral.sh/ruff/rules/redundant-log-base/) (`FURB163`)
- [`regex-flag-alias`](https://docs.astral.sh/ruff/rules/regex-flag-alias/) (`FURB167`)
- [`isinstance-type-none`](https://docs.astral.sh/ruff/rules/isinstance-type-none/) (`FURB168`)
- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`)
- [`implicit-cwd`](https://docs.astral.sh/ruff/rules/implicit-cwd/) (`FURB177`)
- [`hashlib-digest-hex`](https://docs.astral.sh/ruff/rules/hashlib-digest-hex/) (`FURB181`)
- [`list-reverse-copy`](https://docs.astral.sh/ruff/rules/list-reverse-copy/) (`FURB187`)
- [`bad-open-mode`](https://docs.astral.sh/ruff/rules/bad-open-mode/) (`PLW1501`)
- [`empty-comment`](https://docs.astral.sh/ruff/rules/empty-comment/) (`PLR2044`)
- [`global-at-module-level`](https://docs.astral.sh/ruff/rules/global-at-module-level/) (`PLW0604`)
- [`misplaced-bare-raise`](https://docs.astral.sh/ruff/rules/misplaced-bare-raise%60/) (`PLE0744`)
- [`non-ascii-import-name`](https://docs.astral.sh/ruff/rules/non-ascii-import-name/) (`PLC2403`)
- [`non-ascii-name`](https://docs.astral.sh/ruff/rules/non-ascii-name/) (`PLC2401`)
- [`nonlocal-and-global`](https://docs.astral.sh/ruff/rules/nonlocal-and-global/) (`PLE0115`)
- [`potential-index-error`](https://docs.astral.sh/ruff/rules/potential-index-error/) (`PLE0643`)
- [`redeclared-assigned-name`](https://docs.astral.sh/ruff/rules/redeclared-assigned-name/) (`PLW0128`)
- [`redefined-argument-from-local`](https://docs.astral.sh/ruff/rules/redefined-argument-from-local/) (`PLR1704`)
- [`repeated-keyword-argument`](https://docs.astral.sh/ruff/rules/repeated-keyword-argument/) (`PLE1132`)
- [`super-without-brackets`](https://docs.astral.sh/ruff/rules/super-without-brackets/) (`PLW0245`)
- [`unnecessary-list-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-list-index-lookup/) (`PLR1736`)
- [`useless-exception-statement`](https://docs.astral.sh/ruff/rules/useless-exception-statement/) (`PLW0133`)
- [`useless-with-lock`](https://docs.astral.sh/ruff/rules/useless-with-lock/) (`PLW2101`)
The following behaviors have been stabilized:
- [`is-literal`](https://docs.astral.sh/ruff/rules/is-literal/) (`F632`) now warns for identity checks against list, set or dictionary literals
- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) now detects `if` expressions with implicit `else` branches
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) now allows `os.environ` modifications between import statements
- [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison/) (`E721`) now allows idioms such as `type(x) is int`
- [`yoda-condition`](https://docs.astral.sh/ruff/rules/yoda-conditions/) (`SIM300`) now flags a wider range of expressions
### Removals
The following deprecated settings have been removed:
- `output-format=text`; use `output-format=concise` or `output-format=full`
- `tab-size`; use `indent-width`
The following deprecated CLI options have been removed:
- `--show-source`; use `--output-format=full`
- `--no-show-source`; use `--output-format=concise`
The following deprecated CLI commands have been removed:
- `ruff <path>`; use `ruff check <path>`
- `ruff --clean`; use `ruff clean`
- `ruff --generate-shell-completion`; use `ruff generate-shell-completion`
### Preview features
- \[`ruff`\] Add `assert-with-print-message` rule ([#11981](https://github.com/astral-sh/ruff/pull/11981))
### CLI
- Use rule name rather than message in `--statistics` ([#11697](https://github.com/astral-sh/ruff/pull/11697))
- Use the output format `full` by default ([#12010](https://github.com/astral-sh/ruff/pull/12010))
- Don't log syntax errors to the console ([#11902](https://github.com/astral-sh/ruff/pull/11902))
### Rule changes
- \[`ruff`\] Fix false positives if `gettext` is imported using an alias (`RUF027`) ([#12025](https://github.com/astral-sh/ruff/pull/12025))
- \[`numpy`\] Update `trapz` and `in1d` deprecation (`NPY201`) ([#11948](https://github.com/astral-sh/ruff/pull/11948))
- \[`flake8-bandit`\] Modify diagnostic ranges for shell-related rules ([#10667](https://github.com/astral-sh/ruff/pull/10667))
### Server
- Closing an untitled, unsaved notebook document no longer throws an error ([#11942](https://github.com/astral-sh/ruff/pull/11942))
- Support the usage of tildes and environment variables in `logFile` ([#11945](https://github.com/astral-sh/ruff/pull/11945))
- Add option to configure whether to show syntax errors ([#12059](https://github.com/astral-sh/ruff/pull/12059))
### Bug fixes
- \[`pycodestyle`\] Avoid `E203` for f-string debug expression ([#12024](https://github.com/astral-sh/ruff/pull/12024))
- \[`pep8-naming`\] Match import-name ignores against both name and alias (`N812`, `N817`) ([#12033](https://github.com/astral-sh/ruff/pull/12033))
- \[`pyflakes`\] Detect assignments that shadow definitions (`F811`) ([#11961](https://github.com/astral-sh/ruff/pull/11961))
### Parser
- Emit a syntax error for an empty type parameter list ([#12030](https://github.com/astral-sh/ruff/pull/12030))
- Avoid consuming the newline for unterminated strings ([#12067](https://github.com/astral-sh/ruff/pull/12067))
- Do not include the newline in the unterminated string range ([#12017](https://github.com/astral-sh/ruff/pull/12017))
- Use the correct range to highlight line continuation errors ([#12016](https://github.com/astral-sh/ruff/pull/12016))
- Consider 2-character EOL before line continuations ([#12035](https://github.com/astral-sh/ruff/pull/12035))
- Consider line continuation character for re-lexing ([#12008](https://github.com/astral-sh/ruff/pull/12008))
### Other changes
- Upgrade the Unicode table used for measuring the line-length ([#11194](https://github.com/astral-sh/ruff/pull/11194))
- Remove the deprecation error message for the nursery selector ([#10172](https://github.com/astral-sh/ruff/pull/10172))
## 0.4.10
### Parser

View File

@@ -346,8 +346,9 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Run `cargo check`. This should update the lock file with new versions.
1. Create a pull request with the changelog and version updates
1. Merge the PR
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yml) with:
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yaml) with:
- The new version number (without starting `v`)
- The commit hash of the merged release pull request on `main`
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
uploaded anything, you can restart after pushing a fix. If you just need to rerun the build,
@@ -359,8 +360,10 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed.
1. Verify the GitHub release:
1. The Changelog should match the content of `CHANGELOG.md`
1. Publish the GitHub release
1. Open the draft release in the GitHub release section
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Append the contributors from the `bump.sh` script
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

21
Cargo.lock generated
View File

@@ -754,17 +754,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -1985,7 +1974,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.5.0"
version = "0.4.10"
dependencies = [
"anyhow",
"argfile",
@@ -2164,7 +2153,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.5.0"
version = "0.4.10"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2504,7 +2493,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"etcetera",
"dirs 5.0.1",
"glob",
"globset",
"ignore",
@@ -3259,9 +3248,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.13"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unicode_names2"

View File

@@ -58,9 +58,9 @@ countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "5.5.3" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.8.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
@@ -219,60 +219,3 @@ opt-level = 1
[profile.profiling]
inherits = "release"
debug = 1
# The profile that 'cargo dist' will build with.
[profile.dist]
inherits = "release"
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.14.0"
# CI backends to support
ci = ["github"]
# The installers to generate for each app
installers = ["shell", "powershell"]
# The archive format to use for windows builds (defaults .zip)
windows-archive = ".zip"
# The archive format to use for non-windows builds (defaults .tar.xz)
unix-archive = ".tar.gz"
# Target platforms to build apps for (Rust target-triple syntax)
targets = [
"aarch64-apple-darwin",
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"arm-unknown-linux-musleabihf",
"armv7-unknown-linux-gnueabihf",
"armv7-unknown-linux-musleabihf",
"i686-pc-windows-msvc",
"i686-unknown-linux-gnu",
"i686-unknown-linux-musl",
"powerpc64-unknown-linux-gnu",
"powerpc64le-unknown-linux-gnu",
"s390x-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
]
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
auto-includes = false
# Whether cargo-dist should create a Github Release or use an existing draft
create-release = true
# Publish jobs to run in CI
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
# Local artifacts jobs to run in CI
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi"]
# Announcement jobs to run in CI
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
# Skip checking whether the specified configuration files are up to date
allow-dirty = ["ci"]
# Whether to install an updater program
install-updater = false

View File

@@ -152,7 +152,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.5.0
rev: v0.4.10
hooks:
# Run the linter.
- id: ruff
@@ -334,6 +334,7 @@ quality tools, including:
- [flake8-super](https://pypi.org/project/flake8-super/)
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
- [flake8-todos](https://pypi.org/project/flake8-todos/)
- [flake8-trio](https://pypi.org/project/flake8-trio/)
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/astral-sh/ruff/issues/2102))

View File

@@ -16,6 +16,5 @@ jod = "jod" # e.g., `jod-thread`
[default]
extend-ignore-re = [
# Line ignore with trailing "spellchecker:disable-line"
"(?Rm)^.*#\\s*spellchecker:disable-line$",
"LICENSEs",
"(?Rm)^.*#\\s*spellchecker:disable-line$"
]

View File

@@ -1,7 +1,7 @@
[package]
name = "ruff"
version = "0.5.0"
publish = true
version = "0.4.10"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{anyhow, bail};
use anyhow::bail;
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser};
use colored::Colorize;
@@ -18,10 +18,10 @@ use ruff_linter::line_width::LineLength;
use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::{
ExtensionPair, FilePattern, OutputFormat, PatternPrefixPair, PerFileIgnore, PreviewMode,
PythonVersion, UnsafeFixes,
ExtensionPair, FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion,
SerializationFormat, UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
@@ -95,6 +95,7 @@ pub enum Command {
/// Run Ruff on the given files or directories (default).
Check(CheckCommand),
/// Explain a rule (or all rules).
#[clap(alias = "--explain")]
#[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))]
Rule {
/// Rule to explain
@@ -124,9 +125,10 @@ pub enum Command {
output_format: HelpFormat,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
Clean,
/// Generate shell completion.
#[clap(hide = true)]
#[clap(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories.
Format(FormatCommand),
@@ -158,6 +160,13 @@ pub struct CheckCommand {
unsafe_fixes: bool,
#[arg(long, overrides_with("unsafe_fixes"), hide = true)]
no_unsafe_fixes: bool,
/// Show violations with source code.
/// Use `--no-show-source` to disable.
/// (Deprecated: use `--output-format=full` or `--output-format=concise` instead of `--show-source` and `--no-show-source`, respectively)
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Show an enumeration of all fixed lint violations.
/// Use `--no-show-fixes` to disable.
#[arg(long, overrides_with("no_show_fixes"))]
@@ -185,7 +194,7 @@ pub struct CheckCommand {
/// The default serialization format is "concise".
/// In preview mode, the default serialization format is "full".
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
pub output_format: Option<OutputFormat>,
pub output_format: Option<SerializationFormat>,
/// Specify file to write the linter output to (default: stdout).
#[arg(short, long, env = "RUFF_OUTPUT_FILE")]
@@ -356,6 +365,7 @@ pub struct CheckCommand {
long,
// Unsupported default-command arguments.
conflicts_with = "diff",
conflicts_with = "show_source",
conflicts_with = "watch",
)]
pub statistics: bool,
@@ -691,7 +701,11 @@ impl CheckCommand {
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
.map(UnsafeFixes::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
output_format: resolve_output_format(self.output_format)?,
output_format: resolve_output_format(
self.output_format,
resolve_bool_arg(self.show_source, self.no_show_source),
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
),
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
extension: self.extension,
};
@@ -919,15 +933,41 @@ The path `{value}` does not point to a configuration file"
}
}
#[allow(deprecated)]
fn resolve_output_format(
output_format: Option<OutputFormat>,
) -> anyhow::Result<Option<OutputFormat>> {
if let Some(OutputFormat::Text) = output_format {
Err(anyhow!("`--output-format=text` is no longer supported. Use `--output-format=full` or `--output-format=concise` instead."))
} else {
Ok(output_format)
}
output_format: Option<SerializationFormat>,
show_sources: Option<bool>,
preview: bool,
) -> Option<SerializationFormat> {
Some(match (output_format, show_sources) {
(Some(o), None) => o,
(Some(SerializationFormat::Grouped), Some(true)) => {
warn_user!("`--show-source` with `--output-format=grouped` is deprecated, and will not show source files. Use `--output-format=full` to show source information.");
SerializationFormat::Grouped
}
(Some(fmt), Some(true)) => {
warn_user!("The `--show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
fmt
}
(Some(fmt), Some(false)) => {
warn_user!("The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
fmt
}
(None, Some(true)) => {
warn_user!("The `--show-source` argument is deprecated. Use `--output-format=full` instead.");
SerializationFormat::Full
}
(None, Some(false)) => {
warn_user!("The `--no-show-source` argument is deprecated. Use `--output-format=concise` instead.");
SerializationFormat::Concise
}
(None, None) => return None
}).map(|format| match format {
SerializationFormat::Text => {
warn_user!("`--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `{}`.", SerializationFormat::default(preview));
SerializationFormat::default(preview)
},
other => other
})
}
/// CLI settings that are distinct from configuration (commands, lists of files,
@@ -1179,7 +1219,7 @@ struct ExplicitConfigOverrides {
fix_only: Option<bool>,
unsafe_fixes: Option<UnsafeFixes>,
force_exclude: Option<bool>,
output_format: Option<OutputFormat>,
output_format: Option<SerializationFormat>,
show_fixes: Option<bool>,
extension: Option<Vec<ExtensionPair>>,
}

View File

@@ -19,7 +19,7 @@ use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::{DiagnosticKind, Fix};
use ruff_linter::message::{DiagnosticMessage, Message};
use ruff_linter::message::Message;
use ruff_linter::{warn_user, VERSION};
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
@@ -333,14 +333,12 @@ impl FileCache {
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
lint.messages
.iter()
.map(|msg| {
Message::Diagnostic(DiagnosticMessage {
kind: msg.kind.clone(),
range: msg.range,
fix: msg.fix.clone(),
file: file.clone(),
noqa_offset: msg.noqa_offset,
})
.map(|msg| Message {
kind: msg.kind.clone(),
range: msg.range,
fix: msg.fix.clone(),
file: file.clone(),
noqa_offset: msg.noqa_offset,
})
.collect()
};
@@ -414,19 +412,18 @@ impl LintCacheData {
notebook_index: Option<NotebookIndex>,
) -> Self {
let source = if let Some(msg) = messages.first() {
msg.source_file().source_text().to_owned()
msg.file.source_text().to_owned()
} else {
String::new() // No messages, no need to keep the source!
};
let messages = messages
.iter()
.filter_map(|message| message.as_diagnostic_message())
.map(|msg| {
// Make sure that all message use the same source file.
assert_eq!(
&msg.file,
messages.first().unwrap().source_file(),
msg.file,
messages.first().unwrap().file,
"message uses a different source file"
);
CacheMessage {
@@ -574,7 +571,6 @@ mod tests {
use test_case::test_case;
use ruff_cache::CACHE_DIR_NAME;
use ruff_linter::message::Message;
use ruff_linter::settings::flags;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_python_ast::PySourceType;
@@ -637,7 +633,11 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics.messages.iter().any(Message::is_syntax_error) {
if diagnostics
.messages
.iter()
.any(|m| m.kind.name == "SyntaxError")
{
parse_errors.push(path.clone());
}
paths.push(path);

View File

@@ -36,7 +36,7 @@ impl<'a> Explanation<'a> {
message_formats: rule.message_formats(),
fix,
explanation: rule.explanation(),
preview: rule.is_preview(),
preview: rule.is_preview() || rule.is_nursery(),
}
}
}
@@ -62,7 +62,7 @@ fn format_rule_text(rule: Rule) -> String {
output.push('\n');
}
if rule.is_preview() {
if rule.is_preview() || rule.is_nursery() {
output.push_str(
r"This rule is in preview and is not stable. The `--preview` flag is required for use.",
);

View File

@@ -4,7 +4,12 @@ use crate::ExitStatus;
use anyhow::Result;
use ruff_server::Server;
pub(crate) fn run_server(_preview: bool, worker_threads: NonZeroUsize) -> Result<ExitStatus> {
pub(crate) fn run_server(preview: bool, worker_threads: NonZeroUsize) -> Result<ExitStatus> {
if !preview {
tracing::error!("--preview needs to be provided as a command line argument while the server is still unstable.\nFor example: `ruff server --preview`");
return Ok(ExitStatus::Error);
}
let server = Server::new(worker_threads)?;
server.run().map(|()| ExitStatus::Success)

View File

@@ -9,18 +9,19 @@ use std::path::Path;
use anyhow::{Context, Result};
use colored::Colorize;
use log::{debug, warn};
use log::{debug, error, warn};
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::message::{Message, SyntaxErrorMessage};
use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::registry::AsRule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{fs, IOError};
use ruff_linter::{fs, IOError, SyntaxError};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
@@ -54,61 +55,57 @@ impl Diagnostics {
path: Option<&Path>,
settings: &LinterSettings,
) -> Self {
match err {
let diagnostic = match err {
// IO errors.
SourceError::Io(_)
| SourceError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
if settings.rules.enabled(Rule::IOError) {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
),
source_file,
TextSize::default(),
)],
FxHashMap::default(),
)
} else {
match path {
Some(path) => {
warn!(
"{}{}{} {err}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
None => {
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
}
}
Self::default()
}
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
)
}
// Syntax errors.
SourceError::Notebook(
NotebookError::InvalidJson(_)
| NotebookError::InvalidSchema(_)
| NotebookError::InvalidFormat(_),
) => {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::SyntaxError(SyntaxErrorMessage {
message: err.to_string(),
range: TextRange::default(),
file: dummy,
})],
FxHashMap::default(),
)
) => Diagnostic::new(
SyntaxError {
message: err.to_string(),
},
TextRange::default(),
),
};
if settings.rules.enabled(diagnostic.kind.rule()) {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
diagnostic,
dummy,
TextSize::default(),
)],
FxHashMap::default(),
)
} else {
match path {
Some(path) => {
warn!(
"{}{}{} {err}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
None => {
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
}
}
Self::default()
}
}
}
@@ -264,8 +261,8 @@ pub(crate) fn lint_path(
// Lint the file.
let (
LinterResult {
messages,
has_syntax_error: has_error,
data: messages,
error: parse_error,
},
transformed,
fixed,
@@ -334,7 +331,7 @@ pub(crate) fn lint_path(
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
if !has_error {
if parse_error.is_none() {
// `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.
@@ -356,6 +353,13 @@ pub(crate) fn lint_path(
}
}
if let Some(error) = parse_error {
error!(
"{}",
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &transformed)
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
} else {
@@ -400,66 +404,52 @@ pub(crate) fn lint_stdin(
};
// Lint the inputs.
let (LinterResult { messages, .. }, transformed, fixed) =
if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
fixed,
}) = lint_fix(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,
) {
match fix_mode {
flags::FixMode::Apply => {
// Write the contents to stdout, regardless of whether any errors were fixed.
transformed.write(&mut io::stdout().lock())?;
}
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
}
}
flags::FixMode::Generate => {}
let (
LinterResult {
data: messages,
error: parse_error,
},
transformed,
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
fixed,
}) = lint_fix(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,
) {
match fix_mode {
flags::FixMode::Apply => {
// Write the contents to stdout, regardless of whether any errors were fixed.
transformed.write(&mut io::stdout().lock())?;
}
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
&settings.linter,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
}
}
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
flags::FixMode::Generate => {}
}
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
@@ -469,10 +459,37 @@ pub(crate) fn lint_stdin(
source_type,
ParseSource::None,
);
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
}
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
};
}
} else {
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
&settings.linter,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
};
if let Some(error) = parse_error {
error!(
"{}",
DisplayParseError::from_source_kind(error, path.map(Path::to_path_buf), &transformed)
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(

View File

@@ -8,15 +8,15 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use args::{GlobalConfigArgs, ServerCommand};
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::OutputFormat;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
@@ -121,6 +121,7 @@ pub fn run(
command,
global_options,
}: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> {
{
let default_panic_hook = std::panic::take_hook();
@@ -144,8 +145,23 @@ pub fn run(
}));
}
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
// support FORCE_COLOR env var
if let Some(force_color) = std::env::var_os("FORCE_COLOR") {
if force_color.len() > 0 {
colored::control::set_override(true);
}
}
set_up_logging(global_options.log_level())?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command {
Command::Version { output_format } => {
commands::version::version(output_format)?;
@@ -335,10 +351,10 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let preview = pyproject_config.settings.linter.preview.is_enabled();
if cli.watch {
if output_format != OutputFormat::default() {
if output_format != SerializationFormat::default(preview) {
warn_user!(
"`--output-format {}` is always used in watch mode.",
OutputFormat::default()
SerializationFormat::default(preview)
);
}

View File

@@ -2,11 +2,9 @@ use std::process::ExitCode;
use clap::{Parser, Subcommand};
use colored::Colorize;
use log::error;
use ruff::args::{Args, Command};
use ruff::{run, ExitStatus};
use ruff_linter::logging::{set_up_logging, LogLevel};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -25,33 +23,23 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
pub fn main() -> ExitCode {
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
// support FORCE_COLOR env var
if let Some(force_color) = std::env::var_os("FORCE_COLOR") {
if force_color.len() > 0 {
colored::control::set_override(true);
}
}
let args = wild::args_os();
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// We can't use `warn_user` here because logging isn't set up at this point
// and we also don't know if the user runs ruff with quiet.
// Keep the message and pass it to `run` that is responsible for emitting the warning.
let deprecated_alias_error = match args.get(1).and_then(|arg| arg.to_str()) {
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) {
// Deprecated aliases that are handled by clap
Some("--explain") => {
Some("`ruff --explain <RULE>` has been removed. Use `ruff rule <RULE>` instead.")
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.")
}
Some("--clean") => {
Some("`ruff --clean` has been removed. Use `ruff clean` instead.")
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` has been removed. Use `ruff generate-shell-completion <SHELL>` instead.")
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
@@ -63,26 +51,18 @@ pub fn main() -> ExitCode {
&& arg != "-V"
&& arg != "--version"
&& arg != "help" => {
{
Some("`ruff <path>` has been removed. Use `ruff check <path>` instead.")
args.insert(1, "check".into());
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.")
}
},
_ => None
};
if let Some(error) = deprecated_alias_error {
#[allow(clippy::print_stderr)]
if set_up_logging(LogLevel::Default).is_ok() {
error!("{}", error);
} else {
eprintln!("{}", error.red().bold());
}
return ExitCode::FAILURE;
}
let args = Args::parse_from(args);
match run(args) {
match run(args, deprecated_alias_warning) {
Ok(code) => code.into(),
Err(err) => {
#[allow(clippy::print_stderr)]

View File

@@ -13,13 +13,13 @@ use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, Message, MessageKind, PylintEmitter,
RdjsonEmitter, SarifEmitter, TextEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, RdjsonEmitter, SarifEmitter,
TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::registry::Rule;
use ruff_linter::registry::{AsRule, Rule};
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
use crate::diagnostics::{Diagnostics, FixMap};
@@ -36,14 +36,13 @@ bitflags! {
}
#[derive(Serialize)]
struct ExpandedStatistics {
code: Option<SerializeRuleAsCode>,
name: SerializeMessageKindAsTitle,
struct ExpandedStatistics<'a> {
code: SerializeRuleAsCode,
message: &'a str,
count: usize,
fixable: bool,
}
#[derive(Copy, Clone)]
struct SerializeRuleAsCode(Rule);
impl Serialize for SerializeRuleAsCode {
@@ -67,31 +66,8 @@ impl From<Rule> for SerializeRuleAsCode {
}
}
struct SerializeMessageKindAsTitle(MessageKind);
impl Serialize for SerializeMessageKindAsTitle {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}
impl Display for SerializeMessageKindAsTitle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
impl From<MessageKind> for SerializeMessageKindAsTitle {
fn from(kind: MessageKind) -> Self {
Self(kind)
}
}
pub(crate) struct Printer {
format: OutputFormat,
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
@@ -100,7 +76,7 @@ pub(crate) struct Printer {
impl Printer {
pub(crate) const fn new(
format: OutputFormat,
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
@@ -241,13 +217,12 @@ impl Printer {
}
if !self.flags.intersects(Flags::SHOW_VIOLATIONS) {
#[allow(deprecated)]
if matches!(
self.format,
OutputFormat::Text
| OutputFormat::Full
| OutputFormat::Concise
| OutputFormat::Grouped
SerializationFormat::Text
| SerializationFormat::Full
| SerializationFormat::Concise
| SerializationFormat::Grouped
) {
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() {
@@ -265,23 +240,24 @@ impl Printer {
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
match self.format {
OutputFormat::Json => {
SerializationFormat::Json => {
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Rdjson => {
SerializationFormat::Rdjson => {
RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::JsonLines => {
SerializationFormat::JsonLines => {
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Junit => {
SerializationFormat::Junit => {
JunitEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Concise | OutputFormat::Full => {
SerializationFormat::Concise
| SerializationFormat::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_show_source(self.format == SerializationFormat::Full)
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
@@ -295,7 +271,7 @@ impl Printer {
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Grouped => {
SerializationFormat::Grouped => {
GroupedEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
@@ -310,23 +286,22 @@ impl Printer {
}
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Github => {
SerializationFormat::Github => {
GithubEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Gitlab => {
SerializationFormat::Gitlab => {
GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Pylint => {
SerializationFormat::Pylint => {
PylintEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Azure => {
SerializationFormat::Azure => {
AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
}
OutputFormat::Sarif => {
SerializationFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
}
#[allow(deprecated)]
OutputFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
SerializationFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
}
writer.flush()?;
@@ -342,23 +317,30 @@ impl Printer {
let statistics: Vec<ExpandedStatistics> = diagnostics
.messages
.iter()
.sorted_by_key(|message| (message.rule(), message.fixable()))
.fold(vec![], |mut acc: Vec<(&Message, usize)>, message| {
if let Some((prev_message, count)) = acc.last_mut() {
if prev_message.rule() == message.rule() {
.map(|message| {
(
message.kind.rule(),
&message.kind.body,
message.fix.is_some(),
)
})
.sorted()
.fold(vec![], |mut acc, (rule, body, fixable)| {
if let Some((prev_rule, _, _, count)) = acc.last_mut() {
if *prev_rule == rule {
*count += 1;
return acc;
}
}
acc.push((message, 1));
acc.push((rule, body, fixable, 1));
acc
})
.iter()
.map(|&(message, count)| ExpandedStatistics {
code: message.rule().map(std::convert::Into::into),
name: message.kind().into(),
count,
fixable: message.fixable(),
.map(|(rule, message, fixable, count)| ExpandedStatistics {
code: (*rule).into(),
count: *count,
message,
fixable: *fixable,
})
.sorted_by_key(|statistic| Reverse(statistic.count))
.collect();
@@ -368,8 +350,9 @@ impl Printer {
}
match self.format {
#[allow(deprecated)]
OutputFormat::Text | OutputFormat::Full | OutputFormat::Concise => {
SerializationFormat::Text
| SerializationFormat::Full
| SerializationFormat::Concise => {
// Compute the maximum number of digits in the count and code, for all messages,
// to enable pretty-printing.
let count_width = num_digits(
@@ -381,12 +364,7 @@ impl Printer {
);
let code_width = statistics
.iter()
.map(|statistic| {
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.len()
})
.map(|statistic| statistic.code.to_string().len())
.max()
.unwrap();
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
@@ -400,11 +378,7 @@ impl Printer {
writer,
"{:>count_width$}\t{:<code_width$}\t{}{}",
statistic.count.to_string().bold(),
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.red()
.bold(),
statistic.code.to_string().red().bold(),
if any_fixable {
if statistic.fixable {
&fixable
@@ -414,12 +388,12 @@ impl Printer {
} else {
""
},
statistic.name,
statistic.message,
)?;
}
return Ok(());
}
OutputFormat::Json => {
SerializationFormat::Json => {
writeln!(writer, "{}", serde_json::to_string_pretty(&statistics)?)?;
}
_ => {
@@ -554,7 +528,7 @@ impl FixableStatistics {
let mut unapplicable_unsafe = 0;
for message in &diagnostics.messages {
if let Some(fix) = message.fix() {
if let Some(fix) = &message.fix {
if fix.applies(unsafe_fixes.required_applicability()) {
applicable += 1;
} else {

View File

@@ -1,6 +1,6 @@
//! A test suite that ensures deprecated command line options have appropriate warnings / behaviors
use ruff_linter::settings::types::OutputFormat;
use ruff_linter::settings::types::SerializationFormat;
use std::process::Command;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
@@ -9,28 +9,142 @@ const BIN_NAME: &str = "ruff";
const STDIN: &str = "l = 1";
fn ruff_check(output_format: OutputFormat) -> Command {
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.to_string();
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
cmd.arg("check")
.arg("--output-format")
.arg(output_format)
.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");
}
Some(false) => {
cmd.arg("--no-show-source");
}
None => {}
}
cmd.arg("-");
cmd
}
#[test]
#[allow(deprecated)]
fn ensure_output_format_is_deprecated() {
assert_cmd_snapshot!(ruff_check(OutputFormat::Text).pass_stdin(STDIN), @r###"
fn ensure_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(true), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
ruff failed
Cause: `--output-format=text` is no longer supported. Use `--output-format=full` or `--output-format=concise` instead.
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_no_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(false), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_output_format_is_deprecated() {
assert_cmd_snapshot!(ruff_check(None, Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_full_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("full".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
|
1 | l = 1
| ^ E741
|
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=full`.
"###);
}
#[test]
fn ensure_output_format_uses_concise_over_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}

View File

@@ -812,13 +812,14 @@ tab-size = 2
if True:
pass
"), @r###"
success: false
exit_code: 2
success: true
exit_code: 0
----- stdout -----
if True:
pass
----- stderr -----
ruff failed
Cause: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update `[RUFF-TOML-PATH]` to use `indent-width = <value>` instead.
warning: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update your configuration to use `indent-width = <value>` instead.
"###);
});
Ok(())

View File

@@ -116,12 +116,6 @@ fn stdin_error() {
exit_code: 1
----- stdout -----
-:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -140,12 +134,6 @@ fn stdin_filename() {
exit_code: 1
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -175,19 +163,7 @@ import bar # unused import
exit_code: 1
----- stdout -----
bar.py:2:8: F401 [*] `bar` imported but unused
|
2 | import bar # unused import
| ^^^ F401
|
= help: Remove unused import: `bar`
foo.py:2:8: F401 [*] `foo` imported but unused
|
2 | import foo # unused import
| ^^^ F401
|
= help: Remove unused import: `foo`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -209,12 +185,6 @@ fn check_warn_stdin_filename_with_files() {
exit_code: 1
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -235,12 +205,6 @@ fn stdin_source_type_py() {
exit_code: 1
----- stdout -----
TCH.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -472,11 +436,6 @@ fn stdin_fix_jupyter() {
}
----- stderr -----
Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
|
1 | print(x)
| ^ F821
|
Found 3 errors (2 fixed, 1 remaining).
"###);
}
@@ -570,19 +529,7 @@ fn stdin_override_parser_ipynb() {
exit_code: 1
----- stdout -----
Jupyter.py:cell 1:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Jupyter.py:cell 3:1:8: F401 [*] `sys` imported but unused
|
1 | import sys
| ^^^ F401
|
= help: Remove unused import: `sys`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -606,12 +553,6 @@ fn stdin_override_parser_py() {
exit_code: 1
----- stdout -----
F401.ipynb:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -634,14 +575,6 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
----- stderr -----
-:3:4: F634 If test is a tuple, which is always `True`
|
1 | import sys
2 |
3 | if (1, 2):
| ^^^^^^ F634
4 | print(sys.version)
|
Found 2 errors (1 fixed, 1 remaining).
"###);
}
@@ -798,15 +731,11 @@ fn stdin_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
|
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
Found 1 error.
----- stderr -----
error: Failed to parse at 1:16: Expected one or more symbol names after import
"###);
}
@@ -818,65 +747,12 @@ fn stdin_multiple_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
2 | bar =
|
-:2:6: SyntaxError: Expected an expression
|
1 | from foo import
2 | bar =
| ^
|
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
-:2:6: E999 SyntaxError: Expected an expression
Found 2 errors.
----- stderr -----
"###);
}
#[test]
fn parse_error_not_included() {
// Select any rule except for `E999`, syntax error should still be shown.
let mut cmd = RuffCheck::default().args(["--select=I"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^
|
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn deprecated_parse_error_selection() {
let mut cmd = RuffCheck::default().args(["--select=E999"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^
|
Found 1 error.
----- stderr -----
warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not.
error: Failed to parse at 1:16: Expected one or more symbol names after import
"###);
}
@@ -978,41 +854,117 @@ fn show_statistics() {
success: false
exit_code: 1
----- stdout -----
1 F401 [*] unused-import
1 F401 [*] `sys` imported but unused
----- stderr -----
"###);
}
#[test]
fn show_statistics_json() {
fn nursery_prefix() {
// Should only detect RUF90X, but not the unstable test rules
let mut cmd = RuffCheck::default()
.args([
"--select",
"F401",
"--statistics",
"--output-format",
"json",
])
.args(["--select", "RUF9", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("import sys\nimport os\n\nprint(os.getuid())\n"), @r###"
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "F401",
"name": "unused-import",
"count": 1,
"fixable": true
}
]
-: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.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
}
#[test]
fn nursery_all() {
// Should detect RUF90X, but not the unstable test rules
let mut cmd = RuffCheck::default()
.args(["--select", "ALL", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: D100 Missing docstring in public module
-: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 8 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
"###);
}
#[test]
fn nursery_direct() {
// Should fail when a nursery rule is selected without the preview flag
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "RUF912", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Selection of unstable rule `RUF912` without the `--preview` flag is not allowed.
"###);
}
#[test]
fn nursery_group_selector() {
// The NURSERY selector is removed but parses in the CLI for a nicer error message
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "NURSERY", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The `NURSERY` selector was removed. Use the `--preview` flag instead.
"###);
}
#[test]
fn nursery_group_selector_preview_enabled() {
// When preview mode is enabled, we shouldn't suggest using the `--preview` flag.
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "NURSERY", "--preview"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The `NURSERY` selector was removed. Unstable rules should be selected individually or by their respective groups.
"###);
}
#[test]
fn preview_enabled_prefix() {
// All the RUF9XX test rules should be triggered
@@ -1028,8 +980,9 @@ fn preview_enabled_prefix() {
-: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: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 6 errors.
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1052,8 +1005,9 @@ fn preview_enabled_all() {
-: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: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 8 errors.
Found 9 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1191,8 +1145,9 @@ fn preview_enabled_group_ignore() {
-: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: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 6 errors.
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1259,9 +1214,6 @@ fn redirect_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
|
Found 1 error.
----- stderr -----
@@ -1294,9 +1246,6 @@ fn redirect_prefix() {
exit_code: 1
----- stdout -----
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
|
Found 1 error.
----- stderr -----
@@ -1314,9 +1263,6 @@ fn deprecated_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
Found 1 error.
----- stderr -----
@@ -1334,13 +1280,7 @@ fn deprecated_multiple_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
|
Found 2 errors.
----- stderr -----
@@ -1359,13 +1299,7 @@ fn deprecated_indirect() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
|
Found 2 errors.
----- stderr -----
@@ -1443,8 +1377,7 @@ fn unreadable_pyproject_toml() -> Result<()> {
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args).err().context("Unexpected success")?;
let err = run(args, None).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)
@@ -1517,12 +1450,6 @@ fn check_input_from_argfile() -> Result<()> {
exit_code: 1
----- stdout -----
/path/to/a.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -1544,13 +1471,7 @@ fn check_hints_hidden_unsafe_fixes() {
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.
|
|
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1568,11 +1489,6 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
exit_code: 1
----- stdout -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
|
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1591,13 +1507,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
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.
|
|
Found 2 errors.
[*] 1 fixable with the --fix option.
@@ -1617,11 +1527,6 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
exit_code: 1
----- stdout -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
|
Found 1 error.
----- stderr -----
@@ -1639,13 +1544,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
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.
|
|
Found 2 errors.
[*] 2 fixable with the --fix option.
@@ -1667,11 +1566,6 @@ fn fix_applies_safe_fixes_by_default() {
----- stderr -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | # fix from stable-test-rule-safe-fix
| RUF902
|
Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
@@ -1709,11 +1603,6 @@ fn fix_does_not_apply_display_only_fixes() {
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | def add_to_list(item, some_list=[]): ...
| RUF903
|
Found 1 error.
"###);
}
@@ -1732,11 +1621,6 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | def add_to_list(item, some_list=[]): ...
| RUF903
|
Found 1 error.
"###);
}
@@ -1754,9 +1638,6 @@ fn fix_only_unsafe_fixes_available() {
----- stderr -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
@@ -1893,13 +1774,7 @@ extend-unsafe-fixes = ["RUF901"]
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.
|
|
Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
@@ -1931,13 +1806,7 @@ extend-safe-fixes = ["RUF902"]
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.
|
|
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -1971,13 +1840,7 @@ extend-safe-fixes = ["RUF902"]
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.
|
|
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2013,61 +1876,12 @@ extend-safe-fixes = ["RUF9"]
exit_code: 1
----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF900
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF901
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF903
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF920 Hey this is a deprecated test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF920
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF921
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
1 | x = {'a': 1, 'a': 1}
| RUF950
2 | print(('foo'))
3 | print(str('foo'))
|
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2129,12 +1943,6 @@ def log(x, base) -> float:
exit_code: 1
----- stdout -----
-:2:5: D417 Missing argument description in the docstring for `log`: `base`
|
2 | def log(x, base) -> float:
| ^^^ D417
3 | """Calculate natural log of a value
|
Found 1 error.
----- stderr -----
@@ -2165,14 +1973,6 @@ select = ["RUF017"]
exit_code: 1
----- stdout -----
-:3:1: RUF017 Avoid quadratic list summation
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
| ^^^^^^^^^^^^^^^ RUF017
|
= help: Replace with `functools.reduce`
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2205,14 +2005,6 @@ unfixable = ["RUF"]
exit_code: 1
----- stdout -----
-:3:1: RUF017 Avoid quadratic list summation
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
| ^^^^^^^^^^^^^^^ RUF017
|
= help: Replace with `functools.reduce`
Found 1 error.
----- stderr -----

View File

@@ -1618,189 +1618,3 @@ print(
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(())
}

View File

@@ -17,7 +17,7 @@ Settings path: "[BASEPATH]/pyproject.toml"
cache_dir = "[BASEPATH]/.ruff_cache"
fix = false
fix_only = false
output_format = full
output_format = concise
show_fixes = false
unsafe_fixes = hint

View File

@@ -73,7 +73,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
);
// Assert that file contains no parse errors
assert!(!result.has_syntax_error);
assert_eq!(result.error, None);
},
criterion::BatchSize::SmallInput,
);

View File

@@ -64,7 +64,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
output.push('\n');
}
if rule.is_preview() {
if rule.is_preview() || rule.is_nursery() {
output.push_str(
r"This rule is unstable and in [preview](../preview.md). The `--preview` flag is required for use.",
);

View File

@@ -34,7 +34,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
format!("<span title='Rule has been deprecated'>{WARNING_SYMBOL}</span>")
}
#[allow(deprecated)]
RuleGroup::Preview => {
RuleGroup::Preview | RuleGroup::Nursery => {
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
}
RuleGroup::Stable => {

View File

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

View File

@@ -1,27 +1,23 @@
import trio
import urllib.request
import requests
import httpx
async def func():
with trio.fail_after():
...
async def foo():
urllib.request.urlopen("http://example.com/foo/bar").read()
async def func():
with trio.fail_at():
await ...
async def foo():
requests.get()
async def func():
with trio.move_on_after():
...
async def foo():
httpx.get()
async def func():
with trio.move_at():
await ...
async def foo():
requests.post()
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...
async def foo():
httpx.post()

View File

@@ -1,48 +1,53 @@
import io
import os
import subprocess
import time
from pathlib import Path
async def foo():
open("") # ASYNC230
io.open_code("") # ASYNC230
with open(""): # ASYNC230
...
with open("") as f: # ASYNC230
...
with foo(), open(""): # ASYNC230
...
async with open(""): # ASYNC230
...
def foo_sync():
open("")
# Violation cases:
async def func():
open("foo") # ASYNC230
open("foo")
async def func():
time.sleep(1)
async def func():
subprocess.run("foo")
async def func():
subprocess.call("foo")
async def func():
subprocess.foo(0)
async def func():
os.wait4(10)
async def func():
os.wait(12)
# Violation cases for pathlib:
async def func():
Path("foo").open() # ASYNC230
Path("foo").open() # ASYNC101
async def func():
p = Path("foo")
p.open() # ASYNC230
p.open() # ASYNC101
async def func():
with Path("foo").open() as f: # ASYNC230
with Path("foo").open() as f: # ASYNC101
pass
@@ -50,13 +55,13 @@ async def func() -> None:
p = Path("foo")
async def bar():
p.open() # ASYNC230
p.open() # ASYNC101
async def func() -> None:
(p1, p2) = (Path("foo"), Path("bar"))
p1.open() # ASYNC230
p1.open() # ASYNC101
# Non-violation cases for pathlib:

View File

@@ -0,0 +1,13 @@
import os
async def foo():
os.popen()
async def foo():
os.spawnl()
async def foo():
os.fspath("foo")

View File

@@ -1,69 +0,0 @@
import urllib
import requests
import httpx
import urllib3
async def foo():
urllib.request.urlopen("http://example.com/foo/bar").read() # ASYNC210
async def foo():
requests.get() # ASYNC210
async def foo():
httpx.get() # ASYNC210
async def foo():
requests.post() # ASYNC210
async def foo():
httpx.post() # ASYNC210
async def foo():
requests.get() # ASYNC210
requests.get(...) # ASYNC210
requests.get # Ok
print(requests.get()) # ASYNC210
print(requests.get(requests.get())) # ASYNC210
requests.options() # ASYNC210
requests.head() # ASYNC210
requests.post() # ASYNC210
requests.put() # ASYNC210
requests.patch() # ASYNC210
requests.delete() # ASYNC210
requests.foo()
httpx.options("") # ASYNC210
httpx.head("") # ASYNC210
httpx.post("") # ASYNC210
httpx.put("") # ASYNC210
httpx.patch("") # ASYNC210
httpx.delete("") # ASYNC210
httpx.foo() # Ok
urllib3.request() # ASYNC210
urllib3.request(...) # ASYNC210
urllib.request.urlopen("") # ASYNC210
r = {}
r.get("not a sync http client") # Ok
async def bar():
def request():
pass
request() # Ok
def urlopen():
pass
urlopen() # Ok

View File

@@ -1,98 +0,0 @@
import os
import subprocess
# Violation cases:
async def func():
subprocess.run("foo") # ASYNC221
async def func():
subprocess.call("foo") # ASYNC221
async def func():
subprocess.foo(0) # OK
async def func():
os.wait4(10) # ASYNC222
async def func():
os.wait(12) # ASYNC222
async def foo():
await async_fun(
subprocess.getoutput() # ASYNC221
)
subprocess.Popen() # ASYNC220
os.system() # ASYNC221
system()
os.system.anything()
os.anything()
subprocess.run() # ASYNC221
subprocess.call() # ASYNC221
subprocess.check_call() # ASYNC221
subprocess.check_output() # ASYNC221
subprocess.getoutput() # ASYNC221
subprocess.getstatusoutput() # ASYNC221
await async_fun(
subprocess.getoutput() # ASYNC221
)
subprocess.anything()
subprocess.foo()
subprocess.bar.foo()
subprocess()
os.posix_spawn() # ASYNC221
os.posix_spawnp() # ASYNC221
os.spawn()
os.spawn
os.spawnllll()
os.spawnl() # ASYNC221
os.spawnle() # ASYNC221
os.spawnlp() # ASYNC221
os.spawnlpe() # ASYNC221
os.spawnv() # ASYNC221
os.spawnve() # ASYNC221
os.spawnvp() # ASYNC221
os.spawnvpe() # ASYNC221
P_NOWAIT = os.P_NOWAIT
# if mode is given, and is not os.P_WAIT: ASYNC220
os.spawnl(os.P_NOWAIT) # ASYNC220
os.spawnl(P_NOWAIT) # ASYNC220
os.spawnl(mode=os.P_NOWAIT) # ASYNC220
os.spawnl(mode=P_NOWAIT) # ASYNC220
P_WAIT = os.P_WAIT
# if it is P_WAIT, ASYNC221
os.spawnl(P_WAIT) # ASYNC221
os.spawnl(mode=os.P_WAIT) # ASYNC221
os.spawnl(mode=P_WAIT) # ASYNC221
# other weird cases: ASYNC220
os.spawnl(0) # ASYNC220
os.spawnl(1) # ASYNC220
os.spawnl(foo()) # ASYNC220
# ASYNC222
os.wait() # ASYNC222
os.wait3() # ASYNC222
os.wait4() # ASYNC222
os.waitid() # ASYNC222
os.waitpid() # ASYNC222
os.waitpi()
os.waiti()

View File

@@ -1,14 +0,0 @@
import time
import asyncio
async def func():
time.sleep(1) # ASYNC251
def func():
time.sleep(1) # OK
async def func():
asyncio.sleep(1) # OK

View File

@@ -510,7 +510,7 @@ image[:,]
image[:,:,]
lambda x, : x
lambda x, :
# ==> unpack.py <==
def function(

View File

@@ -48,7 +48,3 @@ def f():
if isinstance(a, int) or isinstance(a, float):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
if(isinstance(a, int)) or (isinstance(a, float)):
pass

View File

@@ -0,0 +1,27 @@
import trio
async def func():
with trio.fail_after():
...
async def func():
with trio.fail_at():
await ...
async def func():
with trio.move_on_after():
...
async def func():
with trio.move_at():
await ...
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -26,7 +26,7 @@ async def func() -> None:
await trio.lowlevel.wait_task_rescheduled(foo)
await trio.lowlevel.wait_writable(foo)
# ASYNC105
# TRIO105
trio.aclose_forcefully(foo)
trio.open_file(foo)
trio.open_ssl_over_tcp_listeners(foo, foo)
@@ -55,10 +55,10 @@ async def func() -> None:
async with await trio.open_file(foo): # Ok
pass
async with trio.open_file(foo): # ASYNC105
async with trio.open_file(foo): # TRIO105
pass
def func() -> None:
# ASYNC105 (without fix)
# TRIO105 (without fix)
trio.open_file(foo)

View File

@@ -2,19 +2,19 @@ async def func():
import trio
from trio import sleep
await trio.sleep(0) # ASYNC115
await trio.sleep(0) # TRIO115
await trio.sleep(1) # OK
await trio.sleep(0, 1) # OK
await trio.sleep(...) # OK
await trio.sleep() # OK
trio.sleep(0) # ASYNC115
trio.sleep(0) # TRIO115
foo = 0
trio.sleep(foo) # OK
trio.sleep(1) # OK
time.sleep(0) # OK
sleep(0) # ASYNC115
sleep(0) # TRIO115
bar = "bar"
trio.sleep(bar)
@@ -45,18 +45,18 @@ async def func():
def func():
import trio
trio.run(trio.sleep(0)) # ASYNC115
trio.run(trio.sleep(0)) # TRIO115
from trio import Event, sleep
def func():
sleep(0) # ASYNC115
sleep(0) # TRIO115
async def func():
await sleep(seconds=0) # ASYNC115
await sleep(seconds=0) # TRIO115
def func():

View File

@@ -68,3 +68,49 @@ def func():
np.longfloat(12+34j)
np.lookfor
np.obj2sctype(int)
np.PINF
np.PZERO
np.recfromcsv
np.recfromtxt
np.round_(12.34)
np.safe_eval
np.sctype2char
np.sctypes
np.seterrobj
np.set_numeric_ops
np.set_string_function
np.singlecomplex(12+1j)
np.string_("asdf")
np.source
np.tracemalloc_domain
np.unicode_("asf")
np.who()
np.row_stack(([1,2], [3,4]))
np.alltrue([True, True])
np.anytrue([True, False])
np.cumproduct([1, 2, 3])
np.product([1, 2, 3])

View File

@@ -1,58 +0,0 @@
def func():
import numpy as np
np.obj2sctype(int)
np.PINF
np.PZERO
np.recfromcsv
np.recfromtxt
np.round_(12.34)
np.safe_eval
np.sctype2char
np.sctypes
np.seterrobj
np.set_numeric_ops
np.set_string_function
np.singlecomplex(12+1j)
np.string_("asdf")
np.source
np.tracemalloc_domain
np.unicode_("asf")
np.who()
np.row_stack(([1,2], [3,4]))
np.alltrue([True, True])
np.anytrue([True, False])
np.cumproduct([1, 2, 3])
np.product([1, 2, 3])
np.trapz([1, 2, 3])
np.in1d([1, 2], [1, 3, 5])
np.AxisError
np.ComplexWarning
np.compare_chararrays

View File

@@ -1,16 +0,0 @@
def func():
import numpy as np
np.DTypePromotionError
np.ModuleDeprecationWarning
np.RankWarning
np.TooHardError
np.VisibleDeprecationWarning
np.chararray
np.format_parser

View File

@@ -77,6 +77,3 @@ type Number = int
match(foo):
case(1):
pass
# https://github.com/astral-sh/ruff/issues/12094
pass;

View File

@@ -0,0 +1,4 @@
def x():

View File

@@ -0,0 +1,43 @@
"""Checks use of consider-merging-isinstance"""
# pylint:disable=line-too-long, simplifiable-condition
def isinstances():
"Examples of isinstances"
var = range(10)
# merged
if isinstance(var[1], (int, float)):
pass
result = isinstance(var[2], (int, float))
# not merged
if isinstance(var[3], int) or isinstance(var[3], float) or isinstance(var[3], list) and True: # [consider-merging-isinstance]
pass
result = isinstance(var[4], int) or isinstance(var[4], float) or isinstance(var[5], list) and False # [consider-merging-isinstance]
result = isinstance(var[5], int) or True or isinstance(var[5], float) # [consider-merging-isinstance]
inferred_isinstance = isinstance
result = inferred_isinstance(var[6], int) or inferred_isinstance(var[6], float) or inferred_isinstance(var[6], list) and False # [consider-merging-isinstance]
result = isinstance(var[10], str) or isinstance(var[10], int) and var[8] * 14 or isinstance(var[10], float) and var[5] * 14.4 or isinstance(var[10], list) # [consider-merging-isinstance]
result = isinstance(var[11], int) or isinstance(var[11], int) or isinstance(var[11], float) # [consider-merging-isinstance]
result = isinstance(var[20])
result = isinstance()
# Combination merged and not merged
result = isinstance(var[12], (int, float)) or isinstance(var[12], list) # [consider-merging-isinstance]
# not merged but valid
result = isinstance(var[5], int) and var[5] * 14 or isinstance(var[5], float) and var[5] * 14.4
result = isinstance(var[7], int) or not isinstance(var[7], float)
result = isinstance(var[6], int) or isinstance(var[7], float)
result = isinstance(var[6], int) or isinstance(var[7], int)
result = isinstance(var[6], (float, int)) or False
return result
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
if(isinstance(self.k, int)) or (isinstance(self.k, float)):
...

View File

@@ -110,7 +110,7 @@ def func():
def func():
defaultdict(dict, default_factory=list) # OK
defaultdict(dict, defaultdict=list) # OK
def func():

View File

@@ -15,8 +15,8 @@ use crate::rules::{
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_type_checking, flake8_use_pathlib,
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -505,18 +505,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BlockingHttpCallInAsyncFunction) {
flake8_async::rules::blocking_http_call(checker, call);
}
if checker.enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
if checker.enabled(Rule::OpenSleepOrSubprocessInAsyncFunction) {
flake8_async::rules::open_sleep_or_subprocess_call(checker, call);
}
if checker.any_enabled(&[
Rule::CreateSubprocessInAsyncFunction,
Rule::RunProcessInAsyncFunction,
Rule::WaitForProcessInAsyncFunction,
]) {
flake8_async::rules::blocking_process_invocation(checker, call);
}
if checker.enabled(Rule::BlockingSleepInAsyncFunction) {
flake8_async::rules::blocking_sleep(checker, call);
if checker.enabled(Rule::BlockingOsCallInAsyncFunction) {
flake8_async::rules::blocking_os_call(checker, call);
}
if checker.enabled(Rule::SleepForeverCall) {
flake8_async::rules::sleep_forever_call(checker, call);
@@ -970,10 +963,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::no_implicit_cwd(checker, call);
}
if checker.enabled(Rule::TrioSyncCall) {
flake8_async::rules::sync_call(checker, call);
flake8_trio::rules::sync_call(checker, call);
}
if checker.enabled(Rule::TrioZeroSleepCall) {
flake8_async::rules::zero_sleep_call(checker, call);
flake8_trio::rules::zero_sleep_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryDunderCall) {
pylint::rules::unnecessary_dunder_call(checker, call);
@@ -1522,7 +1515,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::reimplemented_starmap(checker, &generator.into());
}
}
Expr::BoolOp(bool_op) => {
Expr::BoolOp(
bool_op @ ast::ExprBoolOp {
op,
values,
range: _,
},
) => {
if checker.enabled(Rule::RepeatedIsinstanceCalls) {
pylint::rules::repeated_isinstance_calls(checker, expr, *op, values);
}
if checker.enabled(Rule::MultipleStartsEndsWith) {
flake8_pie::rules::multiple_starts_ends_with(checker, expr);
}

View File

@@ -8,11 +8,11 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::{
airflow, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie,
flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
airflow, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_debugger,
flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie, flake8_pyi,
flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_trio, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
@@ -357,7 +357,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::TrioAsyncFunctionWithTimeout) {
flake8_async::rules::async_function_with_timeout(checker, function_def);
flake8_trio::rules::async_function_with_timeout(checker, function_def);
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
@@ -1303,7 +1303,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::useless_with_lock(checker, with_stmt);
}
if checker.enabled(Rule::TrioTimeoutWithoutAwait) {
flake8_async::rules::timeout_without_await(checker, with_stmt, items);
flake8_trio::rules::timeout_without_await(checker, with_stmt, items);
}
}
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
@@ -1320,7 +1320,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
perflint::rules::try_except_in_loop(checker, body);
}
if checker.enabled(Rule::TrioUnneededSleep) {
flake8_async::rules::unneeded_sleep(checker, while_stmt);
flake8_trio::rules::unneeded_sleep(checker, while_stmt);
}
}
Stmt::For(

View File

@@ -462,7 +462,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| imports::is_sys_path_modification(stmt, self.semantic())
|| imports::is_os_environ_modification(stmt, self.semantic()))
|| (self.settings.preview.is_enabled()
&& imports::is_os_environ_modification(stmt, self.semantic())))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}

View File

@@ -57,6 +57,9 @@ pub enum RuleGroup {
Deprecated,
/// The rule has been removed, errors will be displayed on use.
Removed,
/// Legacy category for unstable rules, supports backwards compatible selection.
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
Nursery,
}
#[ruff_macros::map_codes]
@@ -68,39 +71,72 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
Some(match (linter, code) {
// pycodestyle errors
(Pycodestyle, "E101") => (RuleGroup::Stable, rules::pycodestyle::rules::MixedSpacesAndTabs),
(Pycodestyle, "E111") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
(Pycodestyle, "E112") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
(Pycodestyle, "E113") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
(Pycodestyle, "E114") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
(Pycodestyle, "E115") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
(Pycodestyle, "E116") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
(Pycodestyle, "E117") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::OverIndented),
(Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
(Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
(Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
(Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
(Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
(Pycodestyle, "E223") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
(Pycodestyle, "E224") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
(Pycodestyle, "E225") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
(Pycodestyle, "E226") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
(Pycodestyle, "E227") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
(Pycodestyle, "E228") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
(Pycodestyle, "E231") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
(Pycodestyle, "E241") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
(Pycodestyle, "E242") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterComma),
(Pycodestyle, "E251") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
(Pycodestyle, "E252") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
(Pycodestyle, "E261") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
(Pycodestyle, "E262") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
(Pycodestyle, "E265") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
(Pycodestyle, "E266") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
(Pycodestyle, "E271") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
(Pycodestyle, "E272") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
(Pycodestyle, "E273") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
(Pycodestyle, "E274") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
(Pycodestyle, "E275") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E111") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
#[allow(deprecated)]
(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
#[allow(deprecated)]
(Pycodestyle, "E113") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
#[allow(deprecated)]
(Pycodestyle, "E114") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
#[allow(deprecated)]
(Pycodestyle, "E115") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E116") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
#[allow(deprecated)]
(Pycodestyle, "E117") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::OverIndented),
#[allow(deprecated)]
(Pycodestyle, "E201") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
#[allow(deprecated)]
(Pycodestyle, "E202") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
#[allow(deprecated)]
(Pycodestyle, "E203") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
#[allow(deprecated)]
(Pycodestyle, "E211") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
#[allow(deprecated)]
(Pycodestyle, "E221") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E222") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E223") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E224") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E225") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
#[allow(deprecated)]
(Pycodestyle, "E226") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
#[allow(deprecated)]
(Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
#[allow(deprecated)]
(Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
#[allow(deprecated)]
(Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
#[allow(deprecated)]
(Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E262") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E265") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E266") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E271") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E272") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E273") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods),
(Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel),
(Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines),
@@ -125,7 +161,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E742") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousClassName),
(Pycodestyle, "E743") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousFunctionName),
(Pycodestyle, "E902") => (RuleGroup::Stable, rules::pycodestyle::rules::IOError),
(Pycodestyle, "E999") => (RuleGroup::Deprecated, rules::pycodestyle::rules::SyntaxError),
(Pycodestyle, "E999") => (RuleGroup::Stable, rules::pycodestyle::rules::SyntaxError),
// pycodestyle warnings
(Pycodestyle, "W191") => (RuleGroup::Stable, rules::pycodestyle::rules::TabIndentation),
@@ -190,15 +226,16 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
(Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiImportName),
#[allow(deprecated)]
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
(Pylint, "C2701") => (RuleGroup::Preview, rules::pylint::rules::ImportPrivateName),
(Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall),
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "E0100") => (RuleGroup::Stable, rules::pylint::rules::YieldInInit),
(Pylint, "E0101") => (RuleGroup::Stable, rules::pylint::rules::ReturnInInit),
(Pylint, "E0115") => (RuleGroup::Stable, rules::pylint::rules::NonlocalAndGlobal),
(Pylint, "E0115") => (RuleGroup::Preview, rules::pylint::rules::NonlocalAndGlobal),
(Pylint, "E0116") => (RuleGroup::Stable, rules::pylint::rules::ContinueInFinally),
(Pylint, "E0117") => (RuleGroup::Stable, rules::pylint::rules::NonlocalWithoutBinding),
(Pylint, "E0118") => (RuleGroup::Stable, rules::pylint::rules::LoadBeforeGlobalDeclaration),
@@ -213,9 +250,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0309") => (RuleGroup::Preview, rules::pylint::rules::InvalidHashReturnType),
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
(Pylint, "E0643") => (RuleGroup::Stable, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Stable, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Stable, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1141") => (RuleGroup::Preview, rules::pylint::rules::DictIterMissingItems),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
(Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs),
@@ -248,62 +285,64 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Removed, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks),
(Pylint, "R1704") => (RuleGroup::Stable, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1730") => (RuleGroup::Preview, rules::pylint::rules::IfStmtMinMax),
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R2044") => (RuleGroup::Stable, rules::pylint::rules::EmptyComment),
(Pylint, "R2044") => (RuleGroup::Preview, rules::pylint::rules::EmptyComment),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6104") => (RuleGroup::Preview, rules::pylint::rules::NonAugmentedAssignment),
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
(Pylint, "R6301") => (RuleGroup::Preview, rules::pylint::rules::NoSelfUse),
#[allow(deprecated)]
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda),
(Pylint, "W0177") => (RuleGroup::Preview, rules::pylint::rules::NanComparison),
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0128") => (RuleGroup::Stable, rules::pylint::rules::RedeclaredAssignedName),
(Pylint, "W0128") => (RuleGroup::Preview, rules::pylint::rules::RedeclaredAssignedName),
(Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral),
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
(Pylint, "W0133") => (RuleGroup::Stable, rules::pylint::rules::UselessExceptionStatement),
(Pylint, "W0133") => (RuleGroup::Preview, rules::pylint::rules::UselessExceptionStatement),
(Pylint, "W0211") => (RuleGroup::Preview, rules::pylint::rules::BadStaticmethodArgument),
(Pylint, "W0245") => (RuleGroup::Stable, rules::pylint::rules::SuperWithoutBrackets),
(Pylint, "W0245") => (RuleGroup::Preview, rules::pylint::rules::SuperWithoutBrackets),
(Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf),
(Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned),
(Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement),
(Pylint, "W0604") => (RuleGroup::Stable, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0642") => (RuleGroup::Preview, rules::pylint::rules::SelfOrClsAssignment),
(Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException),
(Pylint, "W1501") => (RuleGroup::Stable, rules::pylint::rules::BadOpenMode),
(Pylint, "W1501") => (RuleGroup::Preview, rules::pylint::rules::BadOpenMode),
(Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Stable, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Stable, rules::pylint::rules::SubprocessRunWithoutCheck),
(Pylint, "W1514") => (RuleGroup::Preview, rules::pylint::rules::UnspecifiedEncoding),
(Pylint, "W1641") => (RuleGroup::Preview, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2101") => (RuleGroup::Stable, rules::pylint::rules::UselessWithLock),
#[allow(deprecated)]
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2101") => (RuleGroup::Preview, rules::pylint::rules::UselessWithLock),
(Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName),
(Pylint, "W3201") => (RuleGroup::Preview, rules::pylint::rules::BadDunderMethodName),
#[allow(deprecated)]
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3301") => (RuleGroup::Stable, rules::pylint::rules::NestedMinMax),
// flake8-async
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::TrioTimeoutWithoutAwait),
(Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall),
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::TrioAsyncFunctionWithTimeout),
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::TrioUnneededSleep),
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::TrioZeroSleepCall),
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "101") => (RuleGroup::Stable, rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction),
(Flake8Async, "102") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOsCallInAsyncFunction),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
(Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction),
(Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction),
// flake8-trio
(Flake8Trio, "100") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
(Flake8Trio, "105") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioSyncCall),
(Flake8Trio, "109") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioAsyncFunctionWithTimeout),
(Flake8Trio, "110") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioUnneededSleep),
(Flake8Trio, "115") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioZeroSleepCall),
// flake8-builtins
(Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing),
@@ -476,7 +515,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "911") => (RuleGroup::Stable, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
(Flake8Copyright, "001") => (RuleGroup::Preview, rules::flake8_copyright::rules::MissingCopyrightNotice),
#[allow(deprecated)]
(Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
// pyupgrade
(Pyupgrade, "001") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessMetaclassType),
@@ -661,7 +701,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
(Flake8Bandit, "610") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoExtra),
(Flake8Bandit, "610") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoExtra),
(Flake8Bandit, "611") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoRawSql),
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
@@ -931,9 +971,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
(Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
(Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
@@ -952,6 +992,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
#[cfg(any(feature = "test-rules", test))]
(Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule),
#[cfg(any(feature = "test-rules", test))]
#[allow(deprecated)]
(Ruff, "912") => (RuleGroup::Nursery, rules::ruff::rules::NurseryTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "920") => (RuleGroup::Deprecated, rules::ruff::rules::DeprecatedTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "921") => (RuleGroup::Deprecated, rules::ruff::rules::AnotherDeprecatedTestRule),
@@ -998,7 +1041,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Perflint, "203") => (RuleGroup::Stable, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Stable, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Stable, rules::perflint::rules::ManualListCopy),
(Perflint, "403") => (RuleGroup::Stable, rules::perflint::rules::ManualDictComprehension),
(Perflint, "403") => (RuleGroup::Preview, rules::perflint::rules::ManualDictComprehension),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsFixme),
@@ -1014,15 +1057,18 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// refurb
(Refurb, "101") => (RuleGroup::Preview, rules::refurb::rules::ReadWholeFile),
(Refurb, "103") => (RuleGroup::Preview, rules::refurb::rules::WriteWholeFile),
(Refurb, "105") => (RuleGroup::Stable, rules::refurb::rules::PrintEmptyString),
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
(Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator),
(Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend),
#[allow(deprecated)]
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
(Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat),
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
(Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor),
(Refurb, "131") => (RuleGroup::Preview, rules::refurb::rules::DeleteFullSlice),
(Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax),
(Refurb, "129") => (RuleGroup::Preview, rules::refurb::rules::ReadlinesInFor),
#[allow(deprecated)]
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Preview, rules::refurb::rules::IfExprMinMax),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
@@ -1030,18 +1076,18 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "154") => (RuleGroup::Preview, rules::refurb::rules::RepeatedGlobal),
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),
(Refurb, "167") => (RuleGroup::Stable, rules::refurb::rules::RegexFlagAlias),
(Refurb, "168") => (RuleGroup::Stable, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Stable, rules::refurb::rules::TypeNoneComparison),
(Refurb, "167") => (RuleGroup::Preview, rules::refurb::rules::RegexFlagAlias),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
(Refurb, "177") => (RuleGroup::Stable, rules::refurb::rules::ImplicitCwd),
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
(Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex),
(Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy),
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
(Refurb, "187") => (RuleGroup::Preview, rules::refurb::rules::ListReverseCopy),
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
// flake8-logging

View File

@@ -11,7 +11,7 @@ pub use registry::clap_completion::RuleParser;
#[cfg(feature = "clap")]
pub use rule_selector::clap_completion::RuleSelectorParser;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use rules::pycodestyle::rules::{IOError, SyntaxError};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@@ -5,6 +5,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use log::error;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -25,9 +26,11 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{fix_file, FixResult};
use crate::logging::DisplayParseError;
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule, RuleSet};
use crate::rules::pycodestyle;
#[cfg(any(feature = "test-rules", test))]
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
use crate::settings::types::UnsafeFixes;
@@ -35,19 +38,29 @@ use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
pub struct LinterResult {
/// A collection of diagnostic messages generated by the linter.
pub messages: Vec<Message>,
/// A flag indicating the presence of syntax errors in the source file.
/// If `true`, at least one syntax error was detected in the source file.
pub has_syntax_error: bool,
/// A [`Result`]-like type that returns both data and an error. Used to return
/// diagnostics even in the face of parse errors, since many diagnostics can be
/// generated without a full AST.
pub struct LinterResult<T> {
pub data: T,
pub error: Option<ParseError>,
}
impl<T> LinterResult<T> {
const fn new(data: T, error: Option<ParseError>) -> Self {
Self { data, error }
}
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> LinterResult<U> {
LinterResult::new(f(self.data), self.error)
}
}
pub type FixTable = FxHashMap<Rule, usize>;
pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult,
pub result: LinterResult<Vec<Message>>,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, SourceKind>,
/// The number of fixes applied for each [`Rule`].
@@ -69,9 +82,10 @@ pub fn check_path(
source_kind: &SourceKind,
source_type: PySourceType,
parsed: &Parsed<ModModule>,
) -> Vec<Diagnostic> {
) -> LinterResult<Vec<Diagnostic>> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
let mut error = None;
let tokens = parsed.tokens();
let comment_ranges = indexer.comment_ranges();
@@ -128,53 +142,67 @@ pub fn check_path(
));
}
// Run the AST-based rules only if there are no syntax errors.
if parsed.is_valid() {
let use_ast = settings
// Run the AST-based rules.
let use_ast = settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_ast());
let use_imports = !directives.isort.skip_file
&& settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_ast());
let use_imports = !directives.isort.skip_file
&& settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
));
}
if use_imports {
let import_diagnostics = check_imports(
parsed,
locator,
indexer,
&directives.isort,
settings,
stylist,
package,
source_type,
cell_offsets,
);
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match parsed.as_result() {
Ok(parsed) => {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
));
}
if use_imports {
let import_diagnostics = check_imports(
parsed,
locator,
indexer,
&directives.isort,
settings,
stylist,
package,
source_type,
cell_offsets,
);
diagnostics.extend(import_diagnostics);
diagnostics.extend(import_diagnostics);
}
if use_doc_lines {
doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator));
}
}
if use_doc_lines {
doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator));
Err(parse_errors) => {
// Always add a diagnostic for the syntax error, regardless of whether
// `Rule::SyntaxError` is enabled. We avoid propagating the syntax error
// if it's disabled via any of the usual mechanisms (e.g., `noqa`,
// `per-file-ignores`), and the easiest way to detect that suppression is
// to see if the diagnostic persists to the end of the function.
for parse_error in parse_errors {
pycodestyle::rules::syntax_error(&mut diagnostics, parse_error, locator);
}
// TODO(dhruvmanila): Remove this clone
error = parse_errors.iter().next().cloned();
}
}
}
@@ -216,6 +244,9 @@ pub fn check_path(
Rule::StableTestRuleDisplayOnlyFix => {
test_rules::StableTestRuleDisplayOnlyFix::diagnostic(locator, comment_ranges)
}
Rule::NurseryTestRule => {
test_rules::NurseryTestRule::diagnostic(locator, comment_ranges)
}
Rule::PreviewTestRule => {
test_rules::PreviewTestRule::diagnostic(locator, comment_ranges)
}
@@ -277,7 +308,7 @@ pub fn check_path(
locator,
comment_ranges,
&directives.noqa_line_for,
parsed.is_valid(),
error.is_none(),
&per_file_ignores,
settings,
);
@@ -288,6 +319,23 @@ pub fn check_path(
}
}
// If there was a syntax error, check if it should be discarded.
if error.is_some() {
// If the syntax error was removed by _any_ of the above disablement methods (e.g., a
// `noqa` directive, or a `per-file-ignore`), discard it.
if !diagnostics
.iter()
.any(|diagnostic| diagnostic.kind.rule() == Rule::SyntaxError)
{
error = None;
}
// If the syntax error _diagnostic_ is disabled, discard the _diagnostic_.
if !settings.rules.enabled(Rule::SyntaxError) {
diagnostics.retain(|diagnostic| diagnostic.kind.rule() != Rule::SyntaxError);
}
}
// Remove fixes for any rules marked as unfixable.
for diagnostic in &mut diagnostics {
if !settings.rules.should_fix(diagnostic.kind.rule()) {
@@ -307,7 +355,7 @@ pub fn check_path(
}
}
diagnostics
LinterResult::new(diagnostics, error)
}
const MAX_ITERATIONS: usize = 100;
@@ -341,7 +389,10 @@ pub fn add_noqa_to_path(
);
// Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path(
let LinterResult {
data: diagnostics,
error,
} = check_path(
path,
package,
&locator,
@@ -355,6 +406,19 @@ pub fn add_noqa_to_path(
&parsed,
);
// Log any parse errors.
if let Some(error) = error {
error!(
"{}",
DisplayParseError::from_source_code(
error,
Some(path.to_path_buf()),
&locator.to_source_code(),
source_kind,
)
);
}
// Add any missing `# noqa` pragmas.
// TODO(dhruvmanila): Add support for Jupyter Notebooks
add_noqa(
@@ -378,7 +442,7 @@ pub fn lint_only(
source_kind: &SourceKind,
source_type: PySourceType,
source: ParseSource,
) -> LinterResult {
) -> LinterResult<Vec<Message>> {
let parsed = source.into_parsed(source_kind, source_type);
// Map row and column locations to byte slices (lazily).
@@ -399,7 +463,7 @@ pub fn lint_only(
);
// Generate diagnostics.
let diagnostics = check_path(
let result = check_path(
path,
package,
&locator,
@@ -413,22 +477,12 @@ pub fn lint_only(
&parsed,
);
LinterResult {
messages: diagnostics_to_messages(
diagnostics,
parsed.errors(),
path,
&locator,
&directives,
),
has_syntax_error: !parsed.is_valid(),
}
result.map(|diagnostics| diagnostics_to_messages(diagnostics, path, &locator, &directives))
}
/// Convert from diagnostics to messages.
fn diagnostics_to_messages(
diagnostics: Vec<Diagnostic>,
parse_errors: &[ParseError],
path: &Path,
locator: &Locator,
directives: &Directives,
@@ -444,13 +498,12 @@ fn diagnostics_to_messages(
builder.finish()
});
parse_errors
.iter()
.map(|parse_error| Message::from_parse_error(parse_error, locator, file.deref().clone()))
.chain(diagnostics.into_iter().map(|diagnostic| {
diagnostics
.into_iter()
.map(|diagnostic| {
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
}))
})
.collect()
}
@@ -474,8 +527,8 @@ pub fn lint_fix<'a>(
// As an escape hatch, bail after 100 iterations.
let mut iterations = 0;
// Track whether the _initial_ source code is valid syntax.
let mut is_valid_syntax = false;
// Track whether the _initial_ source code was parseable.
let mut parseable = false;
// Continuously fix until the source code stabilizes.
loop {
@@ -501,7 +554,7 @@ pub fn lint_fix<'a>(
);
// Generate diagnostics.
let diagnostics = check_path(
let result = check_path(
path,
package,
&locator,
@@ -516,21 +569,19 @@ pub fn lint_fix<'a>(
);
if iterations == 0 {
is_valid_syntax = parsed.is_valid();
parseable = result.error.is_none();
} else {
// If the source code was parseable on the first pass, but is no
// longer parseable on a subsequent pass, then we've introduced a
// syntax error. Return the original code.
if is_valid_syntax {
if let Some(error) = parsed.errors().first() {
report_fix_syntax_error(
path,
transformed.source_code(),
error,
fixed.keys().copied(),
);
return Err(anyhow!("Fix introduced a syntax error"));
}
if parseable && result.error.is_some() {
report_fix_syntax_error(
path,
transformed.source_code(),
&result.error.unwrap(),
fixed.keys().copied(),
);
return Err(anyhow!("Fix introduced a syntax error"));
}
}
@@ -539,7 +590,7 @@ pub fn lint_fix<'a>(
code: fixed_contents,
fixes: applied,
source_map,
}) = fix_file(&diagnostics, &locator, unsafe_fixes)
}) = fix_file(&result.data, &locator, unsafe_fixes)
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
@@ -556,20 +607,13 @@ pub fn lint_fix<'a>(
continue;
}
report_failed_to_converge_error(path, transformed.source_code(), &diagnostics);
report_failed_to_converge_error(path, transformed.source_code(), &result.data);
}
return Ok(FixerResult {
result: LinterResult {
messages: diagnostics_to_messages(
diagnostics,
parsed.errors(),
path,
&locator,
&directives,
),
has_syntax_error: !is_valid_syntax,
},
result: result.map(|diagnostics| {
diagnostics_to_messages(diagnostics, path, &locator, &directives)
}),
transformed,
fixed,
});

View File

@@ -3,6 +3,7 @@ use std::io::Write;
use ruff_source_file::SourceLocation;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate error logging commands for Azure Pipelines format.
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
@@ -28,14 +29,12 @@ impl Emitter for AzureEmitter {
writeln!(
writer,
"##vso[task.logissue type=error\
;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}",
;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}",
filename = message.filename(),
line = location.row,
col = location.column,
code = message
.rule()
.map_or_else(String::new, |rule| format!("code={};", rule.noqa_code())),
body = message.body(),
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;
}
@@ -47,9 +46,7 @@ impl Emitter for AzureEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::AzureEmitter;
#[test]
@@ -59,12 +56,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = AzureEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -27,8 +27,8 @@ pub(super) struct Diff<'a> {
impl<'a> Diff<'a> {
pub(crate) fn from_message(message: &'a Message) -> Option<Diff> {
message.fix().map(|fix| Diff {
source_code: message.source_file(),
message.fix.as_ref().map(|fix| Diff {
source_code: &message.file,
fix,
})
}

View File

@@ -4,6 +4,7 @@ use ruff_source_file::SourceLocation;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate error workflow command in GitHub Actions format.
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
@@ -31,8 +32,9 @@ impl Emitter for GithubEmitter {
write!(
writer,
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message.rule().map_or_else(String::new, |rule| format!(" ({})", rule.noqa_code())),
"::error title=Ruff \
({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message.kind.rule().noqa_code(),
file = message.filename(),
row = source_location.row,
column = source_location.column,
@@ -40,19 +42,15 @@ impl Emitter for GithubEmitter {
end_column = end_location.column,
)?;
write!(
writeln!(
writer,
"{path}:{row}:{column}:",
"{path}:{row}:{column}: {code} {body}",
path = relativize_path(message.filename()),
row = location.row,
column = location.column,
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;
if let Some(rule) = message.rule() {
write!(writer, " {}", rule.noqa_code())?;
}
writeln!(writer, " {}", message.body())?;
}
Ok(())
@@ -63,9 +61,7 @@ impl Emitter for GithubEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GithubEmitter;
#[test]
@@ -75,12 +71,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = GithubEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -9,6 +9,7 @@ use serde_json::json;
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate JSON with violations in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
@@ -90,14 +91,8 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);
let description = if let Some(rule) = message.rule() {
format!("({}) {}", rule.noqa_code(), message.body())
} else {
message.body().to_string()
};
let value = json!({
"description": description,
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
"severity": "major",
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
@@ -115,10 +110,18 @@ impl Serialize for SerializedMessages<'_> {
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
let Message {
kind,
range: _,
fix: _fix,
file: _,
noqa_offset: _,
} = message;
let mut hasher = DefaultHasher::new();
salt.hash(&mut hasher);
message.name().hash(&mut hasher);
kind.name.hash(&mut hasher);
project_path.hash(&mut hasher);
hasher.finish()
@@ -128,9 +131,7 @@ fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GitlabEmitter;
#[test]
@@ -141,14 +142,6 @@ mod tests {
assert_snapshot!(redact_fingerprint(&content));
}
#[test]
fn syntax_errors() {
let mut emitter = GitlabEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(redact_fingerprint(&content));
}
// Redact the fingerprint because the default hasher isn't stable across platforms.
fn redact_fingerprint(content: &str) -> String {
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;

View File

@@ -205,9 +205,7 @@ impl std::fmt::Write for PadAdapter<'_> {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GroupedEmitter;
use crate::settings::types::UnsafeFixes;
@@ -219,14 +217,6 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = GroupedEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn show_source() {
let mut emitter = GroupedEmitter::default().with_show_source(true);

View File

@@ -10,6 +10,7 @@ use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
#[derive(Default)]
pub struct JsonEmitter;
@@ -49,22 +50,20 @@ impl Serialize for ExpandedMessages<'_> {
}
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
let source_code = message.source_file().to_source_code();
let source_code = message.file.to_source_code();
let notebook_index = context.notebook_index(message.filename());
let fix = message.fix().map(|fix| {
let fix = message.fix.as_ref().map(|fix| {
json!({
"applicability": fix.applicability(),
"message": message.suggestion(),
"message": message.kind.suggestion.as_deref(),
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index },
})
});
let mut start_location = source_code.source_location(message.start());
let mut end_location = source_code.source_location(message.end());
let mut noqa_location = message
.noqa_offset()
.map(|offset| source_code.source_location(offset));
let mut noqa_location = source_code.source_location(message.noqa_offset);
let mut notebook_cell_index = None;
if let Some(notebook_index) = notebook_index {
@@ -75,19 +74,19 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext)
);
start_location = notebook_index.translate_location(&start_location);
end_location = notebook_index.translate_location(&end_location);
noqa_location = noqa_location.map(|location| notebook_index.translate_location(&location));
noqa_location = notebook_index.translate_location(&noqa_location);
}
json!({
"code": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
"message": message.body(),
"code": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
"message": message.kind.body,
"fix": fix,
"cell": notebook_cell_index,
"location": start_location,
"end_location": end_location,
"filename": message.filename(),
"noqa_row": noqa_location.map(|location| location.row)
"noqa_row": noqa_location.row
})
}
@@ -171,7 +170,7 @@ mod tests {
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages, create_syntax_error_messages,
create_notebook_messages,
};
use crate::message::JsonEmitter;
@@ -183,14 +182,6 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JsonEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonEmitter;

View File

@@ -29,7 +29,7 @@ mod tests {
use crate::message::json_lines::JsonLinesEmitter;
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages, create_syntax_error_messages,
create_notebook_messages,
};
#[test]
@@ -40,14 +40,6 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JsonLinesEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonLinesEmitter;

View File

@@ -8,6 +8,7 @@ use ruff_source_file::SourceLocation;
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::registry::AsRule;
#[derive(Default)]
pub struct JunitEmitter;
@@ -43,7 +44,7 @@ impl Emitter for JunitEmitter {
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.body());
status.set_message(message.kind.body.clone());
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
@@ -56,14 +57,10 @@ impl Emitter for JunitEmitter {
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.body()
body = message.kind.body
));
let mut case = TestCase::new(
if let Some(rule) = message.rule() {
format!("org.ruff.{}", rule.noqa_code())
} else {
"org.ruff".to_string()
},
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
);
let file_path = Path::new(filename);
@@ -91,9 +88,7 @@ impl Emitter for JunitEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::JunitEmitter;
#[test]
@@ -103,12 +98,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -14,17 +14,12 @@ pub use json_lines::JsonLinesEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
pub use rdjson::RdjsonEmitter;
pub use sarif::SarifEmitter;
pub use text::TextEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::NotebookIndex;
use ruff_python_parser::ParseError;
use ruff_source_file::{Locator, SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::logging::DisplayParseErrorType;
use crate::registry::{AsRule, Rule};
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
pub use text::TextEmitter;
mod azure;
mod diff;
@@ -39,17 +34,8 @@ mod rdjson;
mod sarif;
mod text;
/// Message represents either a diagnostic message corresponding to a rule violation or a syntax
/// error message raised by the parser.
#[derive(Debug, PartialEq, Eq)]
pub enum Message {
Diagnostic(DiagnosticMessage),
SyntaxError(SyntaxErrorMessage),
}
/// A diagnostic message corresponding to a rule violation.
#[derive(Debug, PartialEq, Eq)]
pub struct DiagnosticMessage {
pub struct Message {
pub kind: DiagnosticKind,
pub range: TextRange,
pub fix: Option<Fix>,
@@ -57,174 +43,37 @@ pub struct DiagnosticMessage {
pub noqa_offset: TextSize,
}
/// A syntax error message raised by the parser.
#[derive(Debug, PartialEq, Eq)]
pub struct SyntaxErrorMessage {
pub message: String,
pub range: TextRange,
pub file: SourceFile,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageKind {
Diagnostic(Rule),
SyntaxError,
}
impl MessageKind {
pub fn as_str(&self) -> &str {
match self {
MessageKind::Diagnostic(rule) => rule.as_ref(),
MessageKind::SyntaxError => "syntax-error",
}
}
}
impl Message {
/// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation.
pub fn from_diagnostic(
diagnostic: Diagnostic,
file: SourceFile,
noqa_offset: TextSize,
) -> Message {
Message::Diagnostic(DiagnosticMessage {
) -> Self {
Self {
range: diagnostic.range(),
kind: diagnostic.kind,
fix: diagnostic.fix,
file,
noqa_offset,
})
}
/// Create a [`Message`] from the given [`ParseError`].
pub fn from_parse_error(
parse_error: &ParseError,
locator: &Locator,
file: SourceFile,
) -> Message {
// Try to create a non-empty range so that the diagnostic can print a caret at the right
// position. This requires that we retrieve the next character, if any, and take its length
// to maintain char-boundaries.
let len = locator
.after(parse_error.location.start())
.chars()
.next()
.map_or(TextSize::new(0), TextLen::text_len);
Message::SyntaxError(SyntaxErrorMessage {
message: format!(
"SyntaxError: {}",
DisplayParseErrorType::new(&parse_error.error)
),
range: TextRange::at(parse_error.location.start(), len),
file,
})
}
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
match self {
Message::Diagnostic(m) => Some(m),
Message::SyntaxError(_) => None,
}
}
/// Returns `true` if `self` is a syntax error message.
pub const fn is_syntax_error(&self) -> bool {
matches!(self, Message::SyntaxError(_))
}
/// Returns a message kind.
pub fn kind(&self) -> MessageKind {
match self {
Message::Diagnostic(m) => MessageKind::Diagnostic(m.kind.rule()),
Message::SyntaxError(_) => MessageKind::SyntaxError,
}
}
/// Returns the name used to represent the diagnostic.
pub fn name(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.name,
Message::SyntaxError(_) => "SyntaxError",
}
}
/// Returns the message body to display to the user.
pub fn body(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.body,
Message::SyntaxError(m) => &m.message,
}
}
/// Returns the fix suggestion for the violation.
pub fn suggestion(&self) -> Option<&str> {
match self {
Message::Diagnostic(m) => m.kind.suggestion.as_deref(),
Message::SyntaxError(_) => None,
}
}
/// Returns the offset at which the `noqa` comment will be placed if it's a diagnostic message.
pub fn noqa_offset(&self) -> Option<TextSize> {
match self {
Message::Diagnostic(m) => Some(m.noqa_offset),
Message::SyntaxError(_) => None,
}
}
/// Returns the [`Fix`] for the message, if there is any.
pub fn fix(&self) -> Option<&Fix> {
match self {
Message::Diagnostic(m) => m.fix.as_ref(),
Message::SyntaxError(_) => None,
}
}
/// Returns `true` if the message contains a [`Fix`].
pub fn fixable(&self) -> bool {
self.fix().is_some()
}
/// Returns the [`Rule`] corresponding to the diagnostic message.
pub fn rule(&self) -> Option<Rule> {
match self {
Message::Diagnostic(m) => Some(m.kind.rule()),
Message::SyntaxError(_) => None,
}
}
/// Returns the filename for the message.
pub fn filename(&self) -> &str {
self.source_file().name()
self.file.name()
}
/// Computes the start source location for the message.
pub fn compute_start_location(&self) -> SourceLocation {
self.source_file()
.to_source_code()
.source_location(self.start())
self.file.to_source_code().source_location(self.start())
}
/// Computes the end source location for the message.
pub fn compute_end_location(&self) -> SourceLocation {
self.source_file()
.to_source_code()
.source_location(self.end())
}
/// Returns the [`SourceFile`] which the message belongs to.
pub fn source_file(&self) -> &SourceFile {
match self {
Message::Diagnostic(m) => &m.file,
Message::SyntaxError(m) => &m.file,
}
self.file.to_source_code().source_location(self.end())
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(self.source_file(), self.start()).cmp(&(other.source_file(), other.start()))
(&self.file, self.start()).cmp(&(&other.file, other.start()))
}
}
@@ -236,10 +85,7 @@ impl PartialOrd for Message {
impl Ranged for Message {
fn range(&self) -> TextRange {
match self {
Message::Diagnostic(m) => m.range,
Message::SyntaxError(m) => m.range,
}
self.range
}
}
@@ -309,30 +155,11 @@ mod tests {
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_notebook::NotebookIndex;
use ruff_python_parser::{parse_unchecked, Mode};
use ruff_source_file::{Locator, OneIndexed, SourceFileBuilder};
use ruff_source_file::{OneIndexed, SourceFileBuilder};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};
pub(super) fn create_syntax_error_messages() -> Vec<Message> {
let source = r"from os import
if call(foo
def bar():
pass
";
let locator = Locator::new(source);
let source_file = SourceFileBuilder::new("syntax_errors.py", source).finish();
parse_unchecked(source, Mode::Module)
.errors()
.iter()
.map(|parse_error| {
Message::from_parse_error(parse_error, &locator, source_file.clone())
})
.collect()
}
pub(super) fn create_messages() -> Vec<Message> {
let fib = r#"import os

View File

@@ -4,6 +4,7 @@ use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate violations in Pylint format.
/// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter)
@@ -26,20 +27,12 @@ impl Emitter for PylintEmitter {
message.compute_start_location().row
};
let body = if let Some(rule) = message.rule() {
format!(
"[{code}] {body}",
code = rule.noqa_code(),
body = message.body()
)
} else {
message.body().to_string()
};
writeln!(
writer,
"{path}:{row}: {body}",
"{path}:{row}: [{code}] {body}",
path = relativize_path(message.filename()),
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;
}
@@ -51,9 +44,7 @@ impl Emitter for PylintEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::PylintEmitter;
#[test]
@@ -63,12 +54,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = PylintEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -9,6 +9,7 @@ use ruff_source_file::SourceCode;
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message, SourceLocation};
use crate::registry::AsRule;
#[derive(Default)]
pub struct RdjsonEmitter;
@@ -57,34 +58,34 @@ impl Serialize for ExpandedMessages<'_> {
}
fn message_to_rdjson_value(message: &Message) -> Value {
let source_code = message.source_file().to_source_code();
let source_code = message.file.to_source_code();
let start_location = source_code.source_location(message.start());
let end_location = source_code.source_location(message.end());
if let Some(fix) = message.fix() {
if let Some(fix) = message.fix.as_ref() {
json!({
"message": message.body(),
"message": message.kind.body,
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
},
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
})
} else {
json!({
"message": message.body(),
"message": message.kind.body,
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
},
})
}
@@ -124,9 +125,7 @@ fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::RdjsonEmitter;
#[test]
@@ -136,12 +135,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = RdjsonEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -3,16 +3,17 @@ use std::io::Write;
use anyhow::Result;
use serde::{Serialize, Serializer};
use serde_json::json;
use strum::IntoEnumIterator;
use ruff_source_file::OneIndexed;
use crate::codes::Rule;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::{Linter, RuleNamespace};
use crate::registry::{AsRule, Linter, RuleNamespace};
use crate::VERSION;
use strum::IntoEnumIterator;
pub struct SarifEmitter;
impl Emitter for SarifEmitter {
@@ -102,7 +103,7 @@ impl Serialize for SarifRule<'_> {
#[derive(Debug)]
struct SarifResult {
rule: Option<Rule>,
rule: Rule,
level: String,
message: String,
uri: String,
@@ -119,9 +120,9 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.rule(),
rule: message.kind.rule(),
level: "error".to_string(),
message: message.name().to_string(),
message: message.kind.name.clone(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
@@ -139,9 +140,9 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.rule(),
rule: message.kind.rule(),
level: "error".to_string(),
message: message.name().to_string(),
message: message.kind.name.clone(),
uri: path.display().to_string(),
start_line: start_location.row,
start_column: start_location.column,
@@ -174,7 +175,7 @@ impl Serialize for SarifResult {
}
}
}],
"ruleId": self.rule.map(|rule| rule.noqa_code().to_string()),
"ruleId": self.rule.noqa_code().to_string(),
})
.serialize(serializer)
}
@@ -183,9 +184,7 @@ impl Serialize for SarifResult {
#[cfg(test)]
mod tests {
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::SarifEmitter;
fn get_output() -> String {
@@ -199,13 +198,6 @@ mod tests {
serde_json::from_str::<serde_json::Value>(&content).unwrap();
}
#[test]
fn valid_syntax_error_json() {
let mut emitter = SarifEmitter {};
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
serde_json::from_str::<serde_json::Value>(&content).unwrap();
}
#[test]
fn test_results() {
let content = get_output();

View File

@@ -1,6 +0,0 @@
---
source: crates/ruff_linter/src/message/azure.rs
expression: content
---
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;]SyntaxError: Expected one or more symbol names after import
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;]SyntaxError: Expected ')', found newline

View File

@@ -1,6 +0,0 @@
---
source: crates/ruff_linter/src/message/github.rs
expression: content
---
::error title=Ruff,file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
::error title=Ruff,file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: SyntaxError: Expected ')', found newline

View File

@@ -1,30 +0,0 @@
---
source: crates/ruff_linter/src/message/gitlab.rs
expression: redact_fingerprint(&content)
---
[
{
"description": "SyntaxError: Expected one or more symbol names after import",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 2
},
"path": "syntax_errors.py"
},
"severity": "major"
},
{
"description": "SyntaxError: Expected ')', found newline",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 3,
"end": 4
},
"path": "syntax_errors.py"
},
"severity": "major"
}
]

View File

@@ -1,7 +0,0 @@
---
source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
syntax_errors.py:
1:15 SyntaxError: Expected one or more symbol names after import
3:12 SyntaxError: Expected ')', found newline

View File

@@ -1,40 +0,0 @@
---
source: crates/ruff_linter/src/message/json.rs
expression: content
---
[
{
"cell": null,
"code": null,
"end_location": {
"column": 1,
"row": 2
},
"filename": "syntax_errors.py",
"fix": null,
"location": {
"column": 15,
"row": 1
},
"message": "SyntaxError: Expected one or more symbol names after import",
"noqa_row": null,
"url": null
},
{
"cell": null,
"code": null,
"end_location": {
"column": 1,
"row": 4
},
"filename": "syntax_errors.py",
"fix": null,
"location": {
"column": 12,
"row": 3
},
"message": "SyntaxError: Expected ')', found newline",
"noqa_row": null,
"url": null
}
]

View File

@@ -1,6 +0,0 @@
---
source: crates/ruff_linter/src/message/json_lines.rs
expression: content
---
{"cell":null,"code":null,"end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"SyntaxError: Expected one or more symbol names after import","noqa_row":null,"url":null}
{"cell":null,"code":null,"end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"SyntaxError: Expected ')', found newline","noqa_row":null,"url":null}

View File

@@ -1,15 +0,0 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="2" failures="2" errors="0">
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
</testcase>
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
<failure message="SyntaxError: Expected &apos;)&apos;, found newline">line 3, col 12, SyntaxError: Expected &apos;)&apos;, found newline</failure>
</testcase>
</testsuite>
</testsuites>

View File

@@ -1,6 +0,0 @@
---
source: crates/ruff_linter/src/message/pylint.rs
expression: content
---
syntax_errors.py:1: SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3: SyntaxError: Expected ')', found newline

View File

@@ -1,53 +0,0 @@
---
source: crates/ruff_linter/src/message/rdjson.rs
expression: content
---
{
"diagnostics": [
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "syntax_errors.py",
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 15,
"line": 1
}
}
},
"message": "SyntaxError: Expected one or more symbol names after import"
},
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "syntax_errors.py",
"range": {
"end": {
"column": 1,
"line": 4
},
"start": {
"column": 12,
"line": 3
}
}
},
"message": "SyntaxError: Expected ')', found newline"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
1 | from os import
| ^
2 |
3 | if call(foo
4 | def bar():
|
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|

View File

@@ -15,6 +15,7 @@ use crate::fs::relativize_path;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::settings::types::UnsafeFixes;
use crate::text_helpers::ShowNonprinting;
@@ -145,33 +146,28 @@ pub(super) struct RuleCodeAndBody<'a> {
impl Display for RuleCodeAndBody<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let kind = &self.message.kind;
if self.show_fix_status {
if let Some(fix) = self.message.fix() {
if let Some(fix) = self.message.fix.as_ref() {
// Do not display an indicator for unapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) {
if let Some(rule) = self.message.rule() {
write!(f, "{} ", rule.noqa_code().to_string().red().bold())?;
}
return write!(
f,
"{fix}{body}",
"{code} {fix}{body}",
code = kind.rule().noqa_code().to_string().red().bold(),
fix = format_args!("[{}] ", "*".cyan()),
body = self.message.body(),
body = kind.body,
);
}
}
};
if let Some(rule) = self.message.rule() {
write!(
f,
"{code} {body}",
code = rule.noqa_code().to_string().red().bold(),
body = self.message.body(),
)
} else {
f.write_str(self.message.body())
}
write!(
f,
"{code} {body}",
code = kind.rule().noqa_code().to_string().red().bold(),
body = kind.body,
)
}
}
@@ -182,7 +178,11 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let suggestion = self.message.suggestion();
let Message {
kind, file, range, ..
} = self.message;
let suggestion = kind.suggestion.as_deref();
let footer = if suggestion.is_some() {
vec![Annotation {
id: None,
@@ -193,9 +193,9 @@ impl Display for MessageCodeFrame<'_> {
Vec::new()
};
let source_code = self.message.source_file().to_source_code();
let source_code = file.to_source_code();
let content_start_index = source_code.line_index(self.message.start());
let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);
// If we're working with a Jupyter Notebook, skip the lines which are
@@ -218,7 +218,7 @@ impl Display for MessageCodeFrame<'_> {
start_index = start_index.saturating_add(1);
}
let content_end_index = source_code.line_index(self.message.end());
let content_end_index = source_code.line_index(range.end());
let mut end_index = content_end_index
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
@@ -249,7 +249,7 @@ impl Display for MessageCodeFrame<'_> {
let source = replace_whitespace(
source_code.slice(TextRange::new(start_offset, end_offset)),
self.message.range() - start_offset,
range - start_offset,
);
let source_text = source.text.show_nonprinting();
@@ -260,10 +260,7 @@ impl Display for MessageCodeFrame<'_> {
let char_length = source.text[source.annotation_range].chars().count();
let label = self
.message
.rule()
.map_or_else(String::new, |rule| rule.noqa_code().to_string());
let label = kind.rule().noqa_code().to_string();
let snippet = Snippet {
title: None,
@@ -359,7 +356,7 @@ mod tests {
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages, create_syntax_error_messages,
create_notebook_messages,
};
use crate::message::TextEmitter;
use crate::settings::types::UnsafeFixes;
@@ -404,12 +401,4 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = TextEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -1063,7 +1063,7 @@ mod tests {
use crate::generate_noqa_edits;
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
@@ -1380,36 +1380,4 @@ print(
))]
);
}
#[test]
fn syntax_error() {
let path = Path::new("/tmp/foo.txt");
let source = "\
foo;
bar =
";
let diagnostics = [Diagnostic::new(
UselessSemicolon,
TextRange::new(4.into(), 5.into()),
)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(
path,
&diagnostics,
&Locator::new(source),
&comment_ranges,
&[],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(
edits,
vec![Some(Edit::replacement(
" # noqa: E703\n".to_string(),
4.into(),
5.into()
))]
);
}
}

View File

@@ -64,6 +64,9 @@ pub enum Linter {
/// [flake8-async](https://pypi.org/project/flake8-async/)
#[prefix = "ASYNC"]
Flake8Async,
/// [flake8-trio](https://pypi.org/project/flake8-trio/)
#[prefix = "TRIO"]
Flake8Trio,
/// [flake8-bandit](https://pypi.org/project/flake8-bandit/)
#[prefix = "S"]
Flake8Bandit,

View File

@@ -103,18 +103,6 @@ static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
("TRY200", "B904"),
("PGH001", "S307"),
("PGH002", "G010"),
// flake8-trio and flake8-async merged with name flake8-async
("TRIO", "ASYNC1"),
("TRIO1", "ASYNC1"),
("TRIO10", "ASYNC10"),
("TRIO100", "ASYNC100"),
("TRIO105", "ASYNC105"),
("TRIO109", "ASYNC109"),
("TRIO11", "ASYNC11"),
("TRIO110", "ASYNC110"),
("TRIO115", "ASYNC115"),
// Removed in v0.5
("PLR1701", "SIM101"),
// Test redirect by exact code
#[cfg(any(feature = "test-rules", test))]
("RUF940", "RUF950"),

View File

@@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::codes::RuleCodePrefix;
use crate::codes::RuleIter;
use crate::codes::{RuleCodePrefix, RuleGroup};
use crate::registry::{Linter, Rule, RuleNamespace};
use crate::rule_redirects::get_redirect;
use crate::settings::types::PreviewMode;
@@ -15,6 +15,9 @@ use crate::settings::types::PreviewMode;
pub enum RuleSelector {
/// Select all rules (includes rules in preview if enabled)
All,
/// Legacy category to select all rules in the "nursery" which predated preview mode
#[deprecated(note = "The nursery was replaced with 'preview mode' which has no selector")]
Nursery,
/// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters
/// via a single selector.
C,
@@ -62,6 +65,8 @@ impl FromStr for RuleSelector {
// **Changes should be reflected in `parse_no_redirect` as well**
match s {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
@@ -125,6 +130,8 @@ impl RuleSelector {
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
match self {
RuleSelector::All => ("", "ALL"),
#[allow(deprecated)]
RuleSelector::Nursery => ("", "NURSERY"),
RuleSelector::C => ("", "C"),
RuleSelector::T => ("", "T"),
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
@@ -184,6 +191,10 @@ impl RuleSelector {
match self {
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
#[allow(deprecated)]
RuleSelector::Nursery => {
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
}
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
@@ -205,23 +216,19 @@ impl RuleSelector {
pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator<Item = Rule> + 'a {
let preview_enabled = preview.mode.is_enabled();
let preview_require_explicit = preview.require_explicit;
#[allow(deprecated)]
self.all_rules().filter(move |rule| {
match rule.group() {
// Always include stable rules
RuleGroup::Stable => true,
// Enabling preview includes all preview rules unless explicit selection is turned on
RuleGroup::Preview => {
preview_enabled && (self.is_exact() || !preview_require_explicit)
}
// Deprecated rules are excluded in preview mode and with 'All' option unless explicitly selected
RuleGroup::Deprecated => {
(!preview_enabled || self.is_exact())
&& !matches!(self, RuleSelector::All { .. })
}
// Removed rules are included if explicitly selected but will error downstream
RuleGroup::Removed => self.is_exact(),
}
// Always include stable rules
rule.is_stable()
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|| ((self.is_exact() || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
// Enabling preview includes all preview or nursery rules unless explicit selection
// is turned on
|| ((rule.is_preview() || rule.is_nursery()) && preview_enabled && (self.is_exact() || !preview_require_explicit))
// Deprecated rules are excluded in preview mode unless explicitly selected
|| (rule.is_deprecated() && (!preview_enabled || self.is_exact()))
// Removed rules are included if explicitly selected but will error downstream
|| (rule.is_removed() && self.is_exact())
})
}
@@ -233,6 +240,7 @@ impl RuleSelector {
pub enum RuleSelectorIter {
All(RuleIter),
Nursery(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
Vec(std::vec::IntoIter<Rule>),
}
@@ -243,6 +251,7 @@ impl Iterator for RuleSelectorIter {
fn next(&mut self) -> Option<Self::Item> {
match self {
RuleSelectorIter::All(iter) => iter.next(),
RuleSelectorIter::Nursery(iter) => iter.next(),
RuleSelectorIter::Chain(iter) => iter.next(),
RuleSelectorIter::Vec(iter) => iter.next(),
}
@@ -279,7 +288,7 @@ mod schema {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
[
// Include the non-standard "ALL" selectors.
// Include the non-standard "ALL" and "NURSERY" selectors.
"ALL".to_string(),
// Include the legacy "C" and "T" selectors.
"C".to_string(),
@@ -336,6 +345,8 @@ impl RuleSelector {
pub fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
#[allow(deprecated)]
RuleSelector::Nursery => Specificity::All,
RuleSelector::T => Specificity::LinterGroup,
RuleSelector::C => Specificity::LinterGroup,
RuleSelector::Linter(..) => Specificity::Linter,
@@ -358,6 +369,8 @@ impl RuleSelector {
// **Changes should be reflected in `from_str` as well**
match s {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {

View File

@@ -1,5 +1,4 @@
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -14,18 +13,10 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
#[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("ASYNC100.py"))]
#[test_case(Rule::TrioSyncCall, Path::new("ASYNC105.py"))]
#[test_case(Rule::TrioAsyncFunctionWithTimeout, Path::new("ASYNC109.py"))]
#[test_case(Rule::TrioUnneededSleep, Path::new("ASYNC110.py"))]
#[test_case(Rule::TrioZeroSleepCall, Path::new("ASYNC115.py"))]
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC100.py"))]
#[test_case(Rule::OpenSleepOrSubprocessInAsyncFunction, Path::new("ASYNC101.py"))]
#[test_case(Rule::BlockingOsCallInAsyncFunction, Path::new("ASYNC102.py"))]
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))]
#[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -45,7 +45,6 @@ fn is_blocking_http_call(qualified_name: &QualifiedName) -> bool {
matches!(
qualified_name.segments(),
["urllib", "request", "urlopen"]
| ["urllib3", "request"]
| [
"httpx" | "requests",
"get"
@@ -61,7 +60,7 @@ fn is_blocking_http_call(qualified_name: &QualifiedName) -> bool {
)
}
/// ASYNC210
/// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().in_async_context() {
if checker

View File

@@ -0,0 +1,82 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::name::QualifiedName;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not contain calls to blocking synchronous
/// process calls via the `os` module.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.popen()
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// os.popen()
/// ```
#[violation]
pub struct BlockingOsCallInAsyncFunction;
impl Violation for BlockingOsCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call synchronous `os` methods")
}
}
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().seen_module(Modules::OS) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.as_ref()
.is_some_and(is_unsafe_os_method)
{
checker.diagnostics.push(Diagnostic::new(
BlockingOsCallInAsyncFunction,
call.func.range(),
));
}
}
}
}
fn is_unsafe_os_method(qualified_name: &QualifiedName) -> bool {
matches!(
qualified_name.segments(),
[
"os",
"popen"
| "posix_spawn"
| "posix_spawnp"
| "spawnl"
| "spawnle"
| "spawnlp"
| "spawnlpe"
| "spawnv"
| "spawnve"
| "spawnvp"
| "spawnvpe"
| "system"
]
)
}

View File

@@ -1,166 +0,0 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::find_assigned_value;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks that async functions do not create subprocesses with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.popen(cmd)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct CreateSubprocessInAsyncFunction;
impl Violation for CreateSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not create subprocesses with blocking methods")
}
}
/// ## What it does
/// Checks that async functions do not run processes with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// subprocess.run(cmd)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct RunProcessInAsyncFunction;
impl Violation for RunProcessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not run processes with blocking methods")
}
}
/// ## What it does
/// Checks that async functions do not wait on processes with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.waitpid(0)
/// ```
///
/// Use instead:
/// ```python
/// def wait_for_process():
/// os.waitpid(0)
///
///
/// async def foo():
/// await asyncio.loop.run_in_executor(None, wait_for_process)
/// ```
#[violation]
pub struct WaitForProcessInAsyncFunction;
impl Violation for WaitForProcessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not wait on processes with blocking methods")
}
}
/// ASYNC220, ASYNC221, ASYNC222
pub(crate) fn blocking_process_invocation(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().in_async_context() {
return;
}
let Some(diagnostic_kind) =
checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.and_then(|qualified_name| match qualified_name.segments() {
["subprocess", "Popen"] | ["os", "popen"] => {
Some(CreateSubprocessInAsyncFunction.into())
}
["os", "system" | "posix_spawn" | "posix_spawnp"]
| ["subprocess", "run" | "call" | "check_call" | "check_output" | "getoutput"
| "getstatusoutput"] => Some(RunProcessInAsyncFunction.into()),
["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] => {
Some(WaitForProcessInAsyncFunction.into())
}
["os", "spawnl" | "spawnle" | "spawnlp" | "spawnlpe" | "spawnv" | "spawnve"
| "spawnvp" | "spawnvpe"] => {
if is_p_wait(call, checker.semantic()) {
Some(RunProcessInAsyncFunction.into())
} else {
Some(CreateSubprocessInAsyncFunction.into())
}
}
_ => None,
})
else {
return;
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, call.func.range());
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}
fn is_p_wait(call: &ast::ExprCall, semantic: &SemanticModel) -> bool {
let Some(arg) = call.arguments.find_argument("mode", 0) else {
return true;
};
if let Some(qualified_name) = semantic.resolve_qualified_name(arg) {
return matches!(qualified_name.segments(), ["os", "P_WAIT"]);
} else if let Expr::Name(ast::ExprName { id, .. }) = arg {
let Some(value) = find_assigned_value(id, semantic) else {
return false;
};
if let Some(qualified_name) = semantic.resolve_qualified_name(value) {
return matches!(qualified_name.segments(), ["os", "P_WAIT"]);
}
}
false
}

View File

@@ -1,60 +0,0 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::name::QualifiedName;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not call `time.sleep`.
///
/// ## Why is this bad?
/// Blocking an async function via a `time.sleep` call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// `time.sleep`, negating the benefits of asynchronous programming.
///
/// Instead of `time.sleep`, use `asyncio.sleep`.
///
/// ## Example
/// ```python
/// async def fetch():
/// time.sleep(1)
/// ```
///
/// Use instead:
/// ```python
/// async def fetch():
/// await asyncio.sleep(1)
/// ```
#[violation]
pub struct BlockingSleepInAsyncFunction;
impl Violation for BlockingSleepInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `time.sleep`")
}
}
fn is_blocking_sleep(qualified_name: &QualifiedName) -> bool {
matches!(qualified_name.segments(), ["time", "sleep"])
}
/// ASYNC251
pub(crate) fn blocking_sleep(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.as_ref()
.is_some_and(is_blocking_sleep)
{
checker.diagnostics.push(Diagnostic::new(
BlockingSleepInAsyncFunction,
call.func.range(),
));
}
}
}

View File

@@ -1,21 +1,9 @@
pub(crate) use async_function_with_timeout::*;
pub(crate) use blocking_http_call::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
pub(crate) use blocking_os_call::*;
pub(crate) use open_sleep_or_subprocess_call::*;
pub(crate) use sleep_forever_call::*;
pub(crate) use sync_call::*;
pub(crate) use timeout_without_await::*;
pub(crate) use unneeded_sleep::*;
pub(crate) use zero_sleep_call::*;
mod async_function_with_timeout;
mod blocking_http_call;
mod blocking_open_call;
mod blocking_process_invocation;
mod blocking_sleep;
mod blocking_os_call;
mod open_sleep_or_subprocess_call;
mod sleep_forever_call;
mod sync_call;
mod timeout_without_await;
mod unneeded_sleep;
mod zero_sleep_call;

View File

@@ -7,7 +7,8 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not open files with blocking methods like `open`.
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
/// or `subprocess` methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
@@ -20,53 +21,61 @@ use crate::checkers::ast::Checker;
/// ## Example
/// ```python
/// async def foo():
/// with open("bar.txt") as f:
/// contents = f.read()
/// time.sleep(1000)
/// ```
///
/// Use instead:
/// ```python
/// import anyio
///
///
/// async def foo():
/// async with await anyio.open_file("bar.txt") as f:
/// contents = await f.read()
/// await asyncio.sleep(1000)
/// ```
#[violation]
pub struct BlockingOpenCallInAsyncFunction;
pub struct OpenSleepOrSubprocessInAsyncFunction;
impl Violation for BlockingOpenCallInAsyncFunction {
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not open files with blocking methods like `open`")
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
}
}
/// ASYNC230
pub(crate) fn blocking_open_call(checker: &mut Checker, call: &ast::ExprCall) {
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().in_async_context() {
return;
}
if is_open_call(&call.func, checker.semantic())
if is_open_sleep_or_subprocess_call(&call.func, checker.semantic())
|| is_open_call_from_pathlib(call.func.as_ref(), checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
BlockingOpenCallInAsyncFunction,
OpenSleepOrSubprocessInAsyncFunction,
call.func.range(),
));
}
}
/// Returns `true` if the expression resolves to a blocking open call, like `open` or `Path().open()`.
fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
/// Returns `true` if the expression resolves to a blocking call, like `time.sleep` or
/// `subprocess.run`.
fn is_open_sleep_or_subprocess_call(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["" | "io", "open"] | ["io", "open_code"]
["" | "builtins", "open"]
| ["time", "sleep"]
| [
"subprocess",
"run"
| "Popen"
| "call"
| "check_call"
| "check_output"
| "getoutput"
| "getstatusoutput"
]
| ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"]
)
})
}

View File

@@ -1,20 +1,39 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC100.py:5:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
ASYNC100.py:7:5: ASYNC100 Async functions should not call blocking HTTP methods
|
4 | async def func():
5 | with trio.fail_after():
| _____^
6 | | ...
| |___________^ ASYNC100
6 | async def foo():
7 | urllib.request.urlopen("http://example.com/foo/bar").read()
| ^^^^^^^^^^^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:15:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
ASYNC100.py:11:5: ASYNC100 Async functions should not call blocking HTTP methods
|
14 | async def func():
15 | with trio.move_on_after():
| _____^
16 | | ...
| |___________^ ASYNC100
10 | async def foo():
11 | requests.get()
| ^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:15:5: ASYNC100 Async functions should not call blocking HTTP methods
|
14 | async def foo():
15 | httpx.get()
| ^^^^^^^^^ ASYNC100
|
ASYNC100.py:19:5: ASYNC100 Async functions should not call blocking HTTP methods
|
18 | async def foo():
19 | requests.post()
| ^^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:23:5: ASYNC100 Async functions should not call blocking HTTP methods
|
22 | async def foo():
23 | httpx.post()
| ^^^^^^^^^^ ASYNC100
|

View File

@@ -0,0 +1,84 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC101.py:10:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
9 | async def func():
10 | open("foo")
| ^^^^ ASYNC101
|
ASYNC101.py:14:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
13 | async def func():
14 | time.sleep(1)
| ^^^^^^^^^^ ASYNC101
|
ASYNC101.py:18:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
17 | async def func():
18 | subprocess.run("foo")
| ^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:22:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
21 | async def func():
22 | subprocess.call("foo")
| ^^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:30:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
29 | async def func():
30 | os.wait4(10)
| ^^^^^^^^ ASYNC101
|
ASYNC101.py:34:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
33 | async def func():
34 | os.wait(12)
| ^^^^^^^ ASYNC101
|
ASYNC101.py:41:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
40 | async def func():
41 | Path("foo").open() # ASYNC101
| ^^^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:46:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
44 | async def func():
45 | p = Path("foo")
46 | p.open() # ASYNC101
| ^^^^^^ ASYNC101
|
ASYNC101.py:50:10: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
49 | async def func():
50 | with Path("foo").open() as f: # ASYNC101
| ^^^^^^^^^^^^^^^^ ASYNC101
51 | pass
|
ASYNC101.py:58:9: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
57 | async def bar():
58 | p.open() # ASYNC101
| ^^^^^^ ASYNC101
|
ASYNC101.py:64:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
62 | (p1, p2) = (Path("foo"), Path("bar"))
63 |
64 | p1.open() # ASYNC101
| ^^^^^^^ ASYNC101
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC102.py:5:5: ASYNC102 Async functions should not call synchronous `os` methods
|
4 | async def foo():
5 | os.popen()
| ^^^^^^^^ ASYNC102
|
ASYNC102.py:9:5: ASYNC102 Async functions should not call synchronous `os` methods
|
8 | async def foo():
9 | os.spawnl()
| ^^^^^^^^^ ASYNC102
|

View File

@@ -1,16 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC109.py:8:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior
|
8 | async def func(timeout):
| ^^^^^^^ ASYNC109
9 | ...
|
ASYNC109.py:12:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior
|
12 | async def func(timeout=10):
| ^^^^^^^^^^ ASYNC109
13 | ...
|

View File

@@ -1,20 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC110.py:5:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
4 | async def func():
5 | while True:
| _____^
6 | | await trio.sleep(10)
| |____________________________^ ASYNC110
|
ASYNC110.py:10:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
9 | async def func():
10 | while True:
| _____^
11 | | await trio.sleep_until(10)
| |__________________________________^ ASYNC110
|

View File

@@ -1,229 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC210.py:8:5: ASYNC210 Async functions should not call blocking HTTP methods
|
7 | async def foo():
8 | urllib.request.urlopen("http://example.com/foo/bar").read() # ASYNC210
| ^^^^^^^^^^^^^^^^^^^^^^ ASYNC210
|
ASYNC210.py:12:5: ASYNC210 Async functions should not call blocking HTTP methods
|
11 | async def foo():
12 | requests.get() # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
|
ASYNC210.py:16:5: ASYNC210 Async functions should not call blocking HTTP methods
|
15 | async def foo():
16 | httpx.get() # ASYNC210
| ^^^^^^^^^ ASYNC210
|
ASYNC210.py:20:5: ASYNC210 Async functions should not call blocking HTTP methods
|
19 | async def foo():
20 | requests.post() # ASYNC210
| ^^^^^^^^^^^^^ ASYNC210
|
ASYNC210.py:24:5: ASYNC210 Async functions should not call blocking HTTP methods
|
23 | async def foo():
24 | httpx.post() # ASYNC210
| ^^^^^^^^^^ ASYNC210
|
ASYNC210.py:28:5: ASYNC210 Async functions should not call blocking HTTP methods
|
27 | async def foo():
28 | requests.get() # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
29 | requests.get(...) # ASYNC210
30 | requests.get # Ok
|
ASYNC210.py:29:5: ASYNC210 Async functions should not call blocking HTTP methods
|
27 | async def foo():
28 | requests.get() # ASYNC210
29 | requests.get(...) # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
30 | requests.get # Ok
31 | print(requests.get()) # ASYNC210
|
ASYNC210.py:31:11: ASYNC210 Async functions should not call blocking HTTP methods
|
29 | requests.get(...) # ASYNC210
30 | requests.get # Ok
31 | print(requests.get()) # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
32 | print(requests.get(requests.get())) # ASYNC210
|
ASYNC210.py:32:11: ASYNC210 Async functions should not call blocking HTTP methods
|
30 | requests.get # Ok
31 | print(requests.get()) # ASYNC210
32 | print(requests.get(requests.get())) # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
33 |
34 | requests.options() # ASYNC210
|
ASYNC210.py:32:24: ASYNC210 Async functions should not call blocking HTTP methods
|
30 | requests.get # Ok
31 | print(requests.get()) # ASYNC210
32 | print(requests.get(requests.get())) # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
33 |
34 | requests.options() # ASYNC210
|
ASYNC210.py:34:5: ASYNC210 Async functions should not call blocking HTTP methods
|
32 | print(requests.get(requests.get())) # ASYNC210
33 |
34 | requests.options() # ASYNC210
| ^^^^^^^^^^^^^^^^ ASYNC210
35 | requests.head() # ASYNC210
36 | requests.post() # ASYNC210
|
ASYNC210.py:35:5: ASYNC210 Async functions should not call blocking HTTP methods
|
34 | requests.options() # ASYNC210
35 | requests.head() # ASYNC210
| ^^^^^^^^^^^^^ ASYNC210
36 | requests.post() # ASYNC210
37 | requests.put() # ASYNC210
|
ASYNC210.py:36:5: ASYNC210 Async functions should not call blocking HTTP methods
|
34 | requests.options() # ASYNC210
35 | requests.head() # ASYNC210
36 | requests.post() # ASYNC210
| ^^^^^^^^^^^^^ ASYNC210
37 | requests.put() # ASYNC210
38 | requests.patch() # ASYNC210
|
ASYNC210.py:37:5: ASYNC210 Async functions should not call blocking HTTP methods
|
35 | requests.head() # ASYNC210
36 | requests.post() # ASYNC210
37 | requests.put() # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
38 | requests.patch() # ASYNC210
39 | requests.delete() # ASYNC210
|
ASYNC210.py:38:5: ASYNC210 Async functions should not call blocking HTTP methods
|
36 | requests.post() # ASYNC210
37 | requests.put() # ASYNC210
38 | requests.patch() # ASYNC210
| ^^^^^^^^^^^^^^ ASYNC210
39 | requests.delete() # ASYNC210
40 | requests.foo()
|
ASYNC210.py:39:5: ASYNC210 Async functions should not call blocking HTTP methods
|
37 | requests.put() # ASYNC210
38 | requests.patch() # ASYNC210
39 | requests.delete() # ASYNC210
| ^^^^^^^^^^^^^^^ ASYNC210
40 | requests.foo()
|
ASYNC210.py:42:5: ASYNC210 Async functions should not call blocking HTTP methods
|
40 | requests.foo()
41 |
42 | httpx.options("") # ASYNC210
| ^^^^^^^^^^^^^ ASYNC210
43 | httpx.head("") # ASYNC210
44 | httpx.post("") # ASYNC210
|
ASYNC210.py:43:5: ASYNC210 Async functions should not call blocking HTTP methods
|
42 | httpx.options("") # ASYNC210
43 | httpx.head("") # ASYNC210
| ^^^^^^^^^^ ASYNC210
44 | httpx.post("") # ASYNC210
45 | httpx.put("") # ASYNC210
|
ASYNC210.py:44:5: ASYNC210 Async functions should not call blocking HTTP methods
|
42 | httpx.options("") # ASYNC210
43 | httpx.head("") # ASYNC210
44 | httpx.post("") # ASYNC210
| ^^^^^^^^^^ ASYNC210
45 | httpx.put("") # ASYNC210
46 | httpx.patch("") # ASYNC210
|
ASYNC210.py:45:5: ASYNC210 Async functions should not call blocking HTTP methods
|
43 | httpx.head("") # ASYNC210
44 | httpx.post("") # ASYNC210
45 | httpx.put("") # ASYNC210
| ^^^^^^^^^ ASYNC210
46 | httpx.patch("") # ASYNC210
47 | httpx.delete("") # ASYNC210
|
ASYNC210.py:46:5: ASYNC210 Async functions should not call blocking HTTP methods
|
44 | httpx.post("") # ASYNC210
45 | httpx.put("") # ASYNC210
46 | httpx.patch("") # ASYNC210
| ^^^^^^^^^^^ ASYNC210
47 | httpx.delete("") # ASYNC210
48 | httpx.foo() # Ok
|
ASYNC210.py:47:5: ASYNC210 Async functions should not call blocking HTTP methods
|
45 | httpx.put("") # ASYNC210
46 | httpx.patch("") # ASYNC210
47 | httpx.delete("") # ASYNC210
| ^^^^^^^^^^^^ ASYNC210
48 | httpx.foo() # Ok
|
ASYNC210.py:50:5: ASYNC210 Async functions should not call blocking HTTP methods
|
48 | httpx.foo() # Ok
49 |
50 | urllib3.request() # ASYNC210
| ^^^^^^^^^^^^^^^ ASYNC210
51 | urllib3.request(...) # ASYNC210
|
ASYNC210.py:51:5: ASYNC210 Async functions should not call blocking HTTP methods
|
50 | urllib3.request() # ASYNC210
51 | urllib3.request(...) # ASYNC210
| ^^^^^^^^^^^^^^^ ASYNC210
52 |
53 | urllib.request.urlopen("") # ASYNC210
|
ASYNC210.py:53:5: ASYNC210 Async functions should not call blocking HTTP methods
|
51 | urllib3.request(...) # ASYNC210
52 |
53 | urllib.request.urlopen("") # ASYNC210
| ^^^^^^^^^^^^^^^^^^^^^^ ASYNC210
54 |
55 | r = {}
|

View File

@@ -1,77 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC22x.py:31:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
29 | subprocess.getoutput() # ASYNC221
30 | )
31 | subprocess.Popen() # ASYNC220
| ^^^^^^^^^^^^^^^^ ASYNC220
32 | os.system() # ASYNC221
|
ASYNC22x.py:73:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
72 | # if mode is given, and is not os.P_WAIT: ASYNC220
73 | os.spawnl(os.P_NOWAIT) # ASYNC220
| ^^^^^^^^^ ASYNC220
74 | os.spawnl(P_NOWAIT) # ASYNC220
75 | os.spawnl(mode=os.P_NOWAIT) # ASYNC220
|
ASYNC22x.py:74:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
72 | # if mode is given, and is not os.P_WAIT: ASYNC220
73 | os.spawnl(os.P_NOWAIT) # ASYNC220
74 | os.spawnl(P_NOWAIT) # ASYNC220
| ^^^^^^^^^ ASYNC220
75 | os.spawnl(mode=os.P_NOWAIT) # ASYNC220
76 | os.spawnl(mode=P_NOWAIT) # ASYNC220
|
ASYNC22x.py:75:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
73 | os.spawnl(os.P_NOWAIT) # ASYNC220
74 | os.spawnl(P_NOWAIT) # ASYNC220
75 | os.spawnl(mode=os.P_NOWAIT) # ASYNC220
| ^^^^^^^^^ ASYNC220
76 | os.spawnl(mode=P_NOWAIT) # ASYNC220
|
ASYNC22x.py:76:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
74 | os.spawnl(P_NOWAIT) # ASYNC220
75 | os.spawnl(mode=os.P_NOWAIT) # ASYNC220
76 | os.spawnl(mode=P_NOWAIT) # ASYNC220
| ^^^^^^^^^ ASYNC220
77 |
78 | P_WAIT = os.P_WAIT
|
ASYNC22x.py:86:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
85 | # other weird cases: ASYNC220
86 | os.spawnl(0) # ASYNC220
| ^^^^^^^^^ ASYNC220
87 | os.spawnl(1) # ASYNC220
88 | os.spawnl(foo()) # ASYNC220
|
ASYNC22x.py:87:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
85 | # other weird cases: ASYNC220
86 | os.spawnl(0) # ASYNC220
87 | os.spawnl(1) # ASYNC220
| ^^^^^^^^^ ASYNC220
88 | os.spawnl(foo()) # ASYNC220
|
ASYNC22x.py:88:5: ASYNC220 Async functions should not create subprocesses with blocking methods
|
86 | os.spawnl(0) # ASYNC220
87 | os.spawnl(1) # ASYNC220
88 | os.spawnl(foo()) # ASYNC220
| ^^^^^^^^^ ASYNC220
89 |
90 | # ASYNC222
|

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