Compare commits
119 Commits
v0.1.7
...
SIM300-CON
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75564e8a4c | ||
|
|
0a0b0420ed | ||
|
|
6e3d5a78fd | ||
|
|
3f36780904 | ||
|
|
cbe3bf9bde | ||
|
|
4b4160eb48 | ||
|
|
29846f5b09 | ||
|
|
07b293d949 | ||
|
|
5ccc21aea2 | ||
|
|
f5d4019c2b | ||
|
|
bc0bf6f41c | ||
|
|
a2bc635584 | ||
|
|
09296e3e3c | ||
|
|
7c894921df | ||
|
|
be8f8e62b5 | ||
|
|
c97d3ddafb | ||
|
|
a7514295c1 | ||
|
|
0bf7683a3f | ||
|
|
c532089fb3 | ||
|
|
0977fa987b | ||
|
|
b4e101e157 | ||
|
|
2345cf1ec9 | ||
|
|
7489e6b881 | ||
|
|
02f8cc9ad3 | ||
|
|
0854f8cfa4 | ||
|
|
6feea863a6 | ||
|
|
2643f74a5d | ||
|
|
c944d23053 | ||
|
|
7879f9e921 | ||
|
|
935b77ec80 | ||
|
|
93d8c56d41 | ||
|
|
a336c1bc95 | ||
|
|
85b27a994f | ||
|
|
760ff26c06 | ||
|
|
0029b4fd07 | ||
|
|
2c6b534e1f | ||
|
|
6ecf844214 | ||
|
|
82731b8194 | ||
|
|
cd3c2f773f | ||
|
|
3ce145c476 | ||
|
|
db38078ca3 | ||
|
|
c8d6958d15 | ||
|
|
25b2361411 | ||
|
|
d1a7bc38ff | ||
|
|
6c224cec52 | ||
|
|
189e947808 | ||
|
|
28b1aa201b | ||
|
|
c99eae2c08 | ||
|
|
7256b882b9 | ||
|
|
b8fc006e52 | ||
|
|
c014622003 | ||
|
|
b6fb972e6f | ||
|
|
18452cf477 | ||
|
|
cb99815c3e | ||
|
|
45f603000d | ||
|
|
1a65e544c5 | ||
|
|
4d2ee5bf98 | ||
|
|
8314c8bb05 | ||
|
|
cb201bc4a5 | ||
|
|
6c0068eeec | ||
|
|
c306f85691 | ||
|
|
b972455ac7 | ||
|
|
5559827a78 | ||
|
|
cb8eea64a8 | ||
|
|
8e9bf84047 | ||
|
|
2993c342d2 | ||
|
|
108260298f | ||
|
|
a53d59f6bd | ||
|
|
1026ece946 | ||
|
|
f452bf8cad | ||
|
|
07380e0657 | ||
|
|
3aa6a30395 | ||
|
|
efb76ffa64 | ||
|
|
4e461cbf03 | ||
|
|
93417b5644 | ||
|
|
b8dd499b2a | ||
|
|
cd2bf26845 | ||
|
|
6e36dcfefe | ||
|
|
febc69ab48 | ||
|
|
6c2613b44e | ||
|
|
cb8a2f5615 | ||
|
|
b7b137abc8 | ||
|
|
f69a35a021 | ||
|
|
829a808526 | ||
|
|
85fc57e7f9 | ||
|
|
20e33bf514 | ||
|
|
b7dd2b5941 | ||
|
|
e043bd46b5 | ||
|
|
d0d88d9375 | ||
|
|
a224f19903 | ||
|
|
2414298289 | ||
|
|
6bbabceead | ||
|
|
04ec11a73d | ||
|
|
b021ede481 | ||
|
|
96ae9fe685 | ||
|
|
cdac90ef68 | ||
|
|
fcc08894cf | ||
|
|
ebc7ac31cb | ||
|
|
981a0703ed | ||
|
|
946b308197 | ||
|
|
d22ce5372d | ||
|
|
acab5f3cf2 | ||
|
|
06c9f625b6 | ||
|
|
bbb0a0c360 | ||
|
|
9361e22fe9 | ||
|
|
f484df5470 | ||
|
|
af88ffc57e | ||
|
|
b918647927 | ||
|
|
ef7778d794 | ||
|
|
bd443ebe91 | ||
|
|
ee6548d7dd | ||
|
|
b4a050c21d | ||
|
|
958702ded0 | ||
|
|
268d95e911 | ||
|
|
3def18fc21 | ||
|
|
c48ba690eb | ||
|
|
fd49fb935f | ||
|
|
fe54ef08aa | ||
|
|
b7ffd73edd |
36
.github/workflows/ci.yaml
vendored
36
.github/workflows/ci.yaml
vendored
@@ -48,8 +48,8 @@ jobs:
|
||||
- "!crates/ruff_dev/**"
|
||||
- "!crates/ruff_shrinking/**"
|
||||
- scripts/*
|
||||
- .github/workflows/ci.yaml
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
formatter:
|
||||
- Cargo.toml
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
code:
|
||||
- "*/**"
|
||||
- "**/*"
|
||||
- "!**/*.md"
|
||||
- "!docs/**"
|
||||
- "!assets/**"
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
name: "cargo clippy"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
cargo-test-linux:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (linux)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
cargo-test-windows:
|
||||
runs-on: windows-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (windows)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
cargo-test-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (wasm)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo fuzz"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
name: "test scripts"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
name: "cargo udeps"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install nightly Rust toolchain"
|
||||
@@ -338,7 +338,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -362,7 +362,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
@@ -444,7 +444,7 @@ jobs:
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: extractions/setup-just@v1
|
||||
env:
|
||||
@@ -455,7 +455,7 @@ jobs:
|
||||
with:
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -483,7 +483,7 @@ jobs:
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@v4
|
||||
@@ -502,7 +502,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@v1
|
||||
uses: CodSpeedHQ/action@v2
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
|
||||
16
.github/workflows/flake8-to-ruff.yaml
vendored
16
.github/workflows/flake8-to-ruff.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
target: [x64, x86]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.target }}
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
target: [x86_64, i686]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
target: [aarch64, armv7, s390x, ppc64le, ppc64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Build wheels"
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
- i686-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
arch: armv7
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Build wheels"
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- name: "Publish to PyPi"
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
|
||||
45
.github/workflows/release.yaml
vendored
45
.github/workflows/release.yaml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-x86_64-apple-darwin.tar.gz
|
||||
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"
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-aarch64-apple-darwin.tar.gz
|
||||
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"
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
@@ -177,7 +177,7 @@ 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"
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
|
||||
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"
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
|
||||
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"
|
||||
@@ -313,7 +313,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -332,10 +332,10 @@ jobs:
|
||||
image: alpine:latest
|
||||
options: -v ${{ github.workspace }}:/io -w /io
|
||||
run: |
|
||||
apk add py3-pip
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
apk add python3
|
||||
python -m venv .venv
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/ruff check --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
|
||||
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"
|
||||
@@ -369,7 +369,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -388,10 +388,11 @@ jobs:
|
||||
distro: alpine_latest
|
||||
githubToken: ${{ github.token }}
|
||||
install: |
|
||||
apk add py3-pip
|
||||
apk add python3
|
||||
run: |
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
ruff check --help
|
||||
python -m venv .venv
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/ruff check --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -399,7 +400,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
|
||||
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"
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))
|
||||
|
||||
Ruff maintains a list of default exclusions, which now consists of the following patterns:
|
||||
|
||||
- `.bzr`
|
||||
- `.direnv`
|
||||
- `.eggs`
|
||||
- `.git-rewrite`
|
||||
- `.git`
|
||||
- `.hg`
|
||||
- `.ipynb_checkpoints`
|
||||
- `.mypy_cache`
|
||||
- `.nox`
|
||||
- `.pants.d`
|
||||
- `.pyenv`
|
||||
- `.pytest_cache`
|
||||
- `.pytype`
|
||||
- `.ruff_cache`
|
||||
- `.svn`
|
||||
- `.tox`
|
||||
- `.venv`
|
||||
- `.vscode`
|
||||
- `__pypackages__`
|
||||
- `_build`
|
||||
- `buck-out`
|
||||
- `build`
|
||||
- `dist`
|
||||
- `node_modules`
|
||||
- `site-packages`
|
||||
- `venv`
|
||||
|
||||
Previously, the `site-packages` directory was not excluded by default. While `site-packages` tends
|
||||
to be excluded anyway by virtue of the `.venv` exclusion, this may not be the case when using Ruff
|
||||
from VS Code outside a virtual environment.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### The deprecated `format` setting has been removed
|
||||
|
||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.8
|
||||
|
||||
This release includes opt-in support for formatting Python snippets within
|
||||
docstrings via the `docstring-code-format` setting.
|
||||
[Check out the blog post](https://astral.sh/blog/ruff-v0.1.8) for more details!
|
||||
|
||||
### Preview features
|
||||
|
||||
- Add `"preserve"` quote-style to mimic Black's skip-string-normalization ([#8822](https://github.com/astral-sh/ruff/pull/8822))
|
||||
- Implement `prefer_splitting_right_hand_side_of_assignments` preview style ([#8943](https://github.com/astral-sh/ruff/pull/8943))
|
||||
- \[`pycodestyle`\] Add fix for `unexpected-spaces-around-keyword-parameter-equals` ([#9072](https://github.com/astral-sh/ruff/pull/9072))
|
||||
- \[`pycodestyle`\] Add fix for comment-related whitespace rules ([#9075](https://github.com/astral-sh/ruff/pull/9075))
|
||||
- \[`pycodestyle`\] Allow `sys.path` modifications between imports ([#9047](https://github.com/astral-sh/ruff/pull/9047))
|
||||
- \[`refurb`\] Implement `hashlib-digest-hex` (`FURB181`) ([#9077](https://github.com/astral-sh/ruff/pull/9077))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Allow `flake8-type-checking` rules to automatically quote runtime-evaluated references ([#6001](https://github.com/astral-sh/ruff/pull/6001))
|
||||
- Allow transparent cell magics in Jupyter Notebooks ([#8911](https://github.com/astral-sh/ruff/pull/8911))
|
||||
- \[`flake8-annotations`\] Avoid `ANN2xx` fixes for abstract methods with empty bodies ([#9034](https://github.com/astral-sh/ruff/pull/9034))
|
||||
- \[`flake8-self`\] Ignore underscore references in type annotations ([#9036](https://github.com/astral-sh/ruff/pull/9036))
|
||||
- \[`pep8-naming`\] Allow class names when `apps.get_model` is a non-string ([#9065](https://github.com/astral-sh/ruff/pull/9065))
|
||||
- \[`pycodestyle`\] Allow `matplotlib.use` calls to intersperse imports ([#9094](https://github.com/astral-sh/ruff/pull/9094))
|
||||
- \[`pyflakes`\] Support fixing unused assignments in tuples by renaming variables (`F841`) ([#9107](https://github.com/astral-sh/ruff/pull/9107))
|
||||
- \[`pylint`\] Add fix for `subprocess-run-without-check` (`PLW1510`) ([#6708](https://github.com/astral-sh/ruff/pull/6708))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Add `docstring-code-format` knob to enable docstring snippet formatting ([#8854](https://github.com/astral-sh/ruff/pull/8854))
|
||||
- Use double quotes for all docstrings, including single-quoted docstrings ([#9020](https://github.com/astral-sh/ruff/pull/9020))
|
||||
- Implement "dynamic" line width mode for docstring code formatting ([#9098](https://github.com/astral-sh/ruff/pull/9098))
|
||||
- Support reformatting Markdown code blocks ([#9030](https://github.com/astral-sh/ruff/pull/9030))
|
||||
- add support for formatting reStructuredText code snippets ([#9003](https://github.com/astral-sh/ruff/pull/9003))
|
||||
- Avoid trailing comma for single-argument with positional separator ([#9076](https://github.com/astral-sh/ruff/pull/9076))
|
||||
- Fix handling of trailing target comment ([#9051](https://github.com/astral-sh/ruff/pull/9051))
|
||||
|
||||
### CLI
|
||||
|
||||
- Hide unsafe fix suggestions when explicitly disabled ([#9095](https://github.com/astral-sh/ruff/pull/9095))
|
||||
- Add SARIF support to `--output-format` ([#9078](https://github.com/astral-sh/ruff/pull/9078))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Apply unnecessary index rule prior to enumerate rewrite ([#9012](https://github.com/astral-sh/ruff/pull/9012))
|
||||
- \[`flake8-err-msg`\] Allow `EM` fixes even if `msg` variable is defined ([#9059](https://github.com/astral-sh/ruff/pull/9059))
|
||||
- \[`flake8-pie`\] Prevent keyword arguments duplication ([#8450](https://github.com/astral-sh/ruff/pull/8450))
|
||||
- \[`flake8-pie`\] Respect trailing comma in `unnecessary-dict-kwargs` (`PIE804`) ([#9015](https://github.com/astral-sh/ruff/pull/9015))
|
||||
- \[`flake8-raise`\] Avoid removing parentheses on ctypes.WinError ([#9027](https://github.com/astral-sh/ruff/pull/9027))
|
||||
- \[`isort`\] Avoid invalid combination of `force-sort-within-types` and `lines-between-types` ([#9041](https://github.com/astral-sh/ruff/pull/9041))
|
||||
- \[`isort`\] Ensure that from-style imports are always ordered first in `__future__` ([#9039](https://github.com/astral-sh/ruff/pull/9039))
|
||||
- \[`pycodestyle`\] Allow tab indentation before keyword ([#9099](https://github.com/astral-sh/ruff/pull/9099))
|
||||
- \[`pylint`\] Ignore `@overrides` and `@overloads` for `too-many-positional` ([#9000](https://github.com/astral-sh/ruff/pull/9000))
|
||||
- \[`pyupgrade`\] Enable `printf-string-formatting` fix with comments on right-hand side ([#9037](https://github.com/astral-sh/ruff/pull/9037))
|
||||
- \[`refurb`\] Make `math-constant` (`FURB152`) rule more targeted ([#9054](https://github.com/astral-sh/ruff/pull/9054))
|
||||
- \[`refurb`\] Support floating-point base in `redundant-log-base` (`FURB163`) ([#9100](https://github.com/astral-sh/ruff/pull/9100))
|
||||
- \[`ruff`\] Detect `unused-asyncio-dangling-task` (`RUF006`) on unused assignments ([#9060](https://github.com/astral-sh/ruff/pull/9060))
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -556,10 +556,10 @@ examples.
|
||||
|
||||
#### Linux
|
||||
|
||||
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
|
||||
Install `perf` and build `ruff_benchmark` with the `profiling` profile and then run it with perf
|
||||
|
||||
```shell
|
||||
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
|
||||
cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1
|
||||
```
|
||||
|
||||
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
|
||||
@@ -567,8 +567,8 @@ gather enough samples for a good flamegraph (change the 999, the sample rate, an
|
||||
of checks, to your liking)
|
||||
|
||||
```shell
|
||||
cargo build --bin ruff_dev --profile=release-debug
|
||||
perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
|
||||
cargo build --bin ruff_dev --profile=profiling
|
||||
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
|
||||
```
|
||||
|
||||
Then convert the recorded profile
|
||||
@@ -598,7 +598,7 @@ cargo install cargo-instruments
|
||||
Then run the profiler with
|
||||
|
||||
```shell
|
||||
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
|
||||
cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1
|
||||
```
|
||||
|
||||
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
|
||||
|
||||
227
Cargo.lock
generated
227
Cargo.lock
generated
@@ -16,14 +16,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -381,7 +382,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -606,7 +607,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -617,7 +618,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -790,14 +791,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
|
||||
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"windows-sys 0.48.0",
|
||||
"redox_syscall 0.4.1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -808,7 +809,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -827,7 +828,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1122,15 +1123,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e"
|
||||
checksum = "bc74b7abae208af9314a406bd7dcc65091230b6e749c09e07a645885fecf34f9"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"pmutil 0.6.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1481,9 +1482,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -1708,7 +1709,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1810,7 +1811,7 @@ dependencies = [
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"toml 0.8.2",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2062,7 +2063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.2",
|
||||
"anyhow",
|
||||
@@ -2154,7 +2155,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
@@ -2198,7 +2199,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2254,10 +2255,11 @@ dependencies = [
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-width",
|
||||
"unicode_names2",
|
||||
"url",
|
||||
"wsl",
|
||||
]
|
||||
|
||||
@@ -2269,7 +2271,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2450,7 +2452,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2541,7 +2543,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2676,18 +2678,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.190"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d"
|
||||
checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
@@ -2696,13 +2698,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.190"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2764,7 +2766,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2868,7 +2870,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2884,9 +2886,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.39"
|
||||
version = "2.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2973,7 +2975,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2985,7 +2987,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3006,7 +3008,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3089,18 +3091,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
@@ -3110,7 +3100,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.2",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3122,19 +3112,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
@@ -3168,7 +3145,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3183,9 +3160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e05fe4a1c906d94b275d8aeb8ff8b9deaca502aeb59ae8ab500a92b8032ac8"
|
||||
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
@@ -3305,9 +3282,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5506ae2c3c1ccbdf468e52fc5ef536c2ccd981f01273a4cb81aa61021f3a5f"
|
||||
checksum = "ac64ef2f016dc69dfa8283394a70b057066eb054d5fcb6b9eb17bd2ec5097211"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"unicode_names2_generator",
|
||||
@@ -3315,9 +3292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2_generator"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6dfc680313e95bc6637fa278cd7a22390c3c2cd7b8b2bd28755bc6c0fc811e7"
|
||||
checksum = "013f6a731e80f3930de580e55ba41dfa846de4e0fdee4a701f97989cb1597d6a"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"log",
|
||||
@@ -3386,7 +3363,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3480,15 +3457,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
||||
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3514,7 +3491,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3527,9 +3504,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.38"
|
||||
version = "0.3.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d"
|
||||
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3541,13 +3518,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.38"
|
||||
version = "0.3.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
|
||||
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3644,6 +3621,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@@ -3674,6 +3660,21 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -3686,6 +3687,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -3698,6 +3705,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -3710,6 +3723,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -3722,6 +3741,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -3734,6 +3759,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -3746,6 +3777,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -3758,6 +3795,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.15"
|
||||
@@ -3796,3 +3839,23 @@ checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
]
|
||||
|
||||
37
Cargo.toml
37
Cargo.toml
@@ -17,24 +17,24 @@ bitflags = { version = "2.4.1" }
|
||||
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.4.7", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
filetime = { version = "0.2.20" }
|
||||
filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
ignore = { version = "0.4.20" }
|
||||
insta = { version = "1.34.0", feature = ["filters", "glob"] }
|
||||
is-macro = { version = "0.3.0" }
|
||||
is-macro = { version = "0.3.1" }
|
||||
itertools = { version = "0.11.0" }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = { version = "2.6.4" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
once_cell = { version = "1.19.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
proc-macro2 = { version = "1.0.70" }
|
||||
quote = { version = "1.0.23" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
serde = { version = "1.0.190", features = ["derive"] }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = { version = "1.0.108" }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.3.0", features = ["inline"] }
|
||||
@@ -42,15 +42,15 @@ smallvec = { version = "1.11.2" }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.25.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.25.3" }
|
||||
syn = { version = "2.0.39" }
|
||||
syn = { version = "2.0.40" }
|
||||
test-case = { version = "3.2.1" }
|
||||
thiserror = { version = "1.0.50" }
|
||||
toml = { version = "0.7.8" }
|
||||
toml = { version = "0.8.2" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.4" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode_names2 = { version = "1.2.0" }
|
||||
unicode_names2 = { version = "1.2.1" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
@@ -88,7 +88,20 @@ rc_mutex = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
# Note that we set these explicitly, and these values
|
||||
# were chosen based on a trade-off between compile times
|
||||
# and runtime performance[1].
|
||||
#
|
||||
# [1]: https://github.com/astral-sh/ruff/pull/9031
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
|
||||
# Some crates don't change as much but benefit more from
|
||||
# more expensive optimization passes, so we selectively
|
||||
# decrease codegen-units in some cases.
|
||||
[profile.release.package.ruff_python_parser]
|
||||
codegen-units = 1
|
||||
[profile.release.package.ruff_python_ast]
|
||||
codegen-units = 1
|
||||
|
||||
[profile.dev.package.insta]
|
||||
@@ -102,8 +115,8 @@ opt-level = 3
|
||||
[profile.dev.package.ruff_python_parser]
|
||||
opt-level = 1
|
||||
|
||||
# Use the `--profile release-debug` flag to show symbols in release mode.
|
||||
# e.g. `cargo build --profile release-debug`
|
||||
[profile.release-debug]
|
||||
# Use the `--profile profiling` flag to show symbols in release mode.
|
||||
# e.g. `cargo build --profile profiling`
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = 1
|
||||
|
||||
@@ -150,7 +150,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.1.7
|
||||
rev: v0.1.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -194,20 +194,25 @@ exclude = [
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -515,7 +515,7 @@ impl<'a> FormatResults<'a> {
|
||||
if changed > 0 && unchanged > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} {}, {} file{} left unchanged",
|
||||
"{} file{} {}, {} file{} {}",
|
||||
changed,
|
||||
if changed == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
@@ -524,6 +524,10 @@ impl<'a> FormatResults<'a> {
|
||||
},
|
||||
unchanged,
|
||||
if unchanged == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "left unchanged",
|
||||
FormatMode::Check | FormatMode::Diff => "already formatted",
|
||||
},
|
||||
)
|
||||
} else if changed > 0 {
|
||||
writeln!(
|
||||
@@ -539,9 +543,13 @@ impl<'a> FormatResults<'a> {
|
||||
} else if unchanged > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} left unchanged",
|
||||
"{} file{} {}",
|
||||
unchanged,
|
||||
if unchanged == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "left unchanged",
|
||||
FormatMode::Check | FormatMode::Diff => "already formatted",
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
|
||||
@@ -13,7 +13,7 @@ use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
|
||||
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, TextEmitter,
|
||||
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, SarifEmitter, TextEmitter,
|
||||
};
|
||||
use ruff_linter::notify_user;
|
||||
use ruff_linter::registry::{AsRule, Rule};
|
||||
@@ -125,15 +125,7 @@ impl Printer {
|
||||
if let Some(fixables) = fixables {
|
||||
let fix_prefix = format!("[{}]", "*".cyan());
|
||||
|
||||
if self.unsafe_fixes.is_enabled() {
|
||||
if fixables.applicable > 0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"{fix_prefix} {} fixable with the --fix option.",
|
||||
fixables.applicable
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
if self.unsafe_fixes.is_hint() {
|
||||
if fixables.applicable > 0 && fixables.unapplicable_unsafe > 0 {
|
||||
let es = if fixables.unapplicable_unsafe == 1 {
|
||||
""
|
||||
@@ -163,6 +155,14 @@ impl Printer {
|
||||
fixables.unapplicable_unsafe
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
if fixables.applicable > 0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"{fix_prefix} {} fixable with the --fix option.",
|
||||
fixables.applicable
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -291,6 +291,9 @@ impl Printer {
|
||||
SerializationFormat::Azure => {
|
||||
AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Sarif => {
|
||||
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
|
||||
@@ -139,6 +139,99 @@ if condition:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docstring_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[format]
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 20
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def f(x):
|
||||
'''
|
||||
Something about `f`. And an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
||||
|
||||
Another example:
|
||||
|
||||
```py
|
||||
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
||||
```
|
||||
|
||||
And another:
|
||||
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
||||
'''
|
||||
pass
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def f(x):
|
||||
"""
|
||||
Something about `f`. And an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(
|
||||
foo,
|
||||
bar,
|
||||
quux,
|
||||
) = this_is_a_long_line(
|
||||
lion,
|
||||
hippo,
|
||||
lemur,
|
||||
bear,
|
||||
)
|
||||
|
||||
Another example:
|
||||
|
||||
```py
|
||||
(
|
||||
foo,
|
||||
bar,
|
||||
quux,
|
||||
) = this_is_a_long_line(
|
||||
lion,
|
||||
hippo,
|
||||
lemur,
|
||||
bear,
|
||||
)
|
||||
```
|
||||
|
||||
And another:
|
||||
|
||||
>>> (
|
||||
... foo,
|
||||
... bar,
|
||||
... quux,
|
||||
... ) = this_is_a_long_line(
|
||||
... lion,
|
||||
... hippo,
|
||||
... lemur,
|
||||
... bear,
|
||||
... )
|
||||
"""
|
||||
pass
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_line_endings() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
@@ -162,7 +255,7 @@ fn mixed_line_endings() -> Result<()> {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
2 files left unchanged
|
||||
2 files already formatted
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -235,6 +328,60 @@ OTHER = "OTHER"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("main.py"),
|
||||
r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == "__main__":
|
||||
say_hy("dear Ruff contributor")
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated", "--check"])
|
||||
.arg("main.py"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Would reformat: main.py
|
||||
1 file would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated"])
|
||||
.arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated"])
|
||||
.arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file left unchanged
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_exclude() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
@@ -783,7 +930,7 @@ fn test_diff() {
|
||||
|
||||
|
||||
----- stderr -----
|
||||
2 files would be reformatted, 1 file left unchanged
|
||||
2 files would be reformatted, 1 file already formatted
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1158,6 +1158,44 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "F601,UP034", "--no-unsafe-fixes"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:14: F601 Dictionary key literal `'a'` repeated
|
||||
-:2:7: UP034 [*] Avoid extraneous parentheses
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the --fix option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "F601", "--no-unsafe-fixes"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("x = {'a': 1, 'a': 1}\n"),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:14: F601 Dictionary key literal `'a'` repeated
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_shows_unsafe_fixes_with_opt_in() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
|
||||
@@ -7,7 +7,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
/// A text edit to be applied to a source file. Inserts, deletes, or replaces
|
||||
/// content at a given location.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Edit {
|
||||
/// The start location of the edit.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -71,6 +71,7 @@ toml = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { workspace = true }
|
||||
url = { version = "2.2.2" }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -147,3 +147,68 @@ def func(x: int):
|
||||
while x > 0:
|
||||
break
|
||||
return 1
|
||||
|
||||
|
||||
import abc
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class Foo(abc.ABC):
|
||||
@abstractmethod
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def method(self):
|
||||
"""Docstring."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def method(self):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def method():
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def method(cls):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def method(self):
|
||||
if self.x > 0:
|
||||
return 1
|
||||
else:
|
||||
return 1.5
|
||||
|
||||
|
||||
def func(x: int):
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
return 2
|
||||
|
||||
|
||||
def func(x: int):
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if not x:
|
||||
raise ValueError
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if not x:
|
||||
raise ValueError
|
||||
else:
|
||||
return 1
|
||||
|
||||
@@ -8,6 +8,7 @@ def func(address):
|
||||
# Error
|
||||
"0.0.0.0"
|
||||
'0.0.0.0'
|
||||
f"0.0.0.0"
|
||||
|
||||
|
||||
# Error
|
||||
|
||||
@@ -5,6 +5,9 @@ with open("/abc/tmp", "w") as f:
|
||||
with open("/tmp/abc", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
with open(f"/tmp/abc", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
with open("/var/tmp/123", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
|
||||
@@ -127,3 +127,21 @@ class MultipleConsecutiveFields(models.Model):
|
||||
pass
|
||||
|
||||
middle_name = models.CharField(max_length=32)
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class StrBeforeFieldInheritedModel(BaseModel):
|
||||
"""Model with `__str__` before fields."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "test"
|
||||
verbose_name_plural = "tests"
|
||||
|
||||
def __str__(self):
|
||||
return "foobar"
|
||||
|
||||
first_name = models.CharField(max_length=32)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def f_ok():
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def f_unfixable():
|
||||
def f_msg_defined():
|
||||
msg = "hello"
|
||||
raise RuntimeError("This is an example exception")
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ Foo.objects.create(**{**bar}) # PIE804
|
||||
|
||||
foo(**{})
|
||||
|
||||
|
||||
foo(**{**data, "foo": "buzz"})
|
||||
foo(**buzz)
|
||||
foo(**{"bar-foo": True})
|
||||
@@ -20,3 +19,8 @@ foo(**{buzz: True})
|
||||
foo(**{"": True})
|
||||
foo(**{f"buzz__{bar}": True})
|
||||
abc(**{"for": 3})
|
||||
foo(**{},)
|
||||
|
||||
# Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import typing
|
||||
import typing_extensions
|
||||
from typing import TypeVar
|
||||
from typing_extensions import ParamSpec, TypeVarTuple
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_P = TypeVar("_P")
|
||||
_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
_P = ParamSpec("_P")
|
||||
_P2 = typing.ParamSpec("_P2")
|
||||
_Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
# OK
|
||||
_UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import typing
|
||||
import typing_extensions
|
||||
from typing import TypeVar
|
||||
from typing_extensions import ParamSpec, TypeVarTuple
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_P = TypeVar("_P")
|
||||
_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
_P = ParamSpec("_P")
|
||||
_P2 = typing.ParamSpec("_P2")
|
||||
_Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
# OK
|
||||
_UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
|
||||
@@ -32,6 +32,7 @@ def f8(x: bytes = b"50 character byte stringgggggggggggggggggggggggggg\xff") ->
|
||||
|
||||
foo: str = "50 character stringggggggggggggggggggggggggggggggg"
|
||||
bar: str = "51 character stringgggggggggggggggggggggggggggggggg"
|
||||
baz: str = f"51 character stringgggggggggggggggggggggggggggggggg"
|
||||
|
||||
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg"
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
|
||||
|
||||
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
|
||||
|
||||
ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
||||
|
||||
fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
|
||||
|
||||
class Demo:
|
||||
"""Docstrings are excluded from this rule. Some padding.""" # OK
|
||||
|
||||
|
||||
@@ -37,3 +37,28 @@ def func():
|
||||
|
||||
# PYI055
|
||||
x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
|
||||
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
@@ -82,3 +82,14 @@ raise IndexError();
|
||||
|
||||
# RSE102
|
||||
raise Foo()
|
||||
|
||||
# OK
|
||||
raise ctypes.WinError()
|
||||
|
||||
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# OK
|
||||
raise func()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Errors
|
||||
"yoda" == compare # SIM300
|
||||
"yoda" == compare # SIM300
|
||||
42 == age # SIM300
|
||||
("a", "b") == compare # SIM300
|
||||
"yoda" <= compare # SIM300
|
||||
@@ -13,10 +12,17 @@ YODA > age # SIM300
|
||||
YODA >= age # SIM300
|
||||
JediOrder.YODA == age # SIM300
|
||||
0 < (number - 100) # SIM300
|
||||
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
B<A[0][0]or B
|
||||
B or(B)<A[0][0]
|
||||
|
||||
# Errors in preview
|
||||
['upper'] == UPPER_LIST
|
||||
{} == DummyHandler.CONFIG
|
||||
|
||||
# Errors in stable
|
||||
UPPER_LIST == ['upper']
|
||||
DummyHandler.CONFIG == {}
|
||||
|
||||
# OK
|
||||
compare == "yoda"
|
||||
age == 42
|
||||
@@ -31,3 +37,6 @@ age <= YODA
|
||||
YODA == YODA
|
||||
age == JediOrder.YODA
|
||||
(number - 100) > 0
|
||||
SECONDS_IN_DAY == 60 * 60 * 24 # Error in 0.1.8
|
||||
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # Error in 0.1.8
|
||||
{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
|
||||
|
||||
@@ -19,8 +19,32 @@ async def func():
|
||||
bar = "bar"
|
||||
trio.sleep(bar)
|
||||
|
||||
x, y = 0, 2000
|
||||
trio.sleep(x) # TRIO115
|
||||
trio.sleep(y) # OK
|
||||
|
||||
(a, b, [c, (d, e)]) = (1, 2, (0, [4, 0]))
|
||||
trio.sleep(c) # TRIO115
|
||||
trio.sleep(d) # OK
|
||||
trio.sleep(e) # TRIO115
|
||||
|
||||
m_x, m_y = 0
|
||||
trio.sleep(m_y) # OK
|
||||
trio.sleep(m_x) # OK
|
||||
|
||||
m_a = m_b = 0
|
||||
trio.sleep(m_a) # TRIO115
|
||||
trio.sleep(m_b) # TRIO115
|
||||
|
||||
m_c = (m_d, m_e) = (0, 0)
|
||||
trio.sleep(m_c) # OK
|
||||
trio.sleep(m_d) # TRIO115
|
||||
trio.sleep(m_e) # TRIO115
|
||||
|
||||
|
||||
def func():
|
||||
import trio
|
||||
|
||||
trio.run(trio.sleep(0)) # TRIO115
|
||||
|
||||
|
||||
@@ -33,3 +57,10 @@ def func():
|
||||
|
||||
async def func():
|
||||
await sleep(seconds=0) # TRIO115
|
||||
|
||||
|
||||
def func():
|
||||
import trio
|
||||
|
||||
if (walrus := 0) == 0:
|
||||
trio.sleep(walrus) # TRIO115
|
||||
|
||||
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_1.py
vendored
Normal file
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_1.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_2.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_2.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Add `TYPE_CHECKING` to an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
Const: Final[dict] = {}
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, TYPE_CHECKING
|
||||
|
||||
Const: Final[dict] = {}
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, Mapping
|
||||
|
||||
Const: Final[dict] = {}
|
||||
92
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/quote.py
vendored
Normal file
92
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/quote.py
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
def f():
|
||||
from pandas import DataFrame
|
||||
|
||||
def baz() -> DataFrame:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame
|
||||
|
||||
def baz() -> DataFrame[int]:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame
|
||||
|
||||
def baz() -> DataFrame["int"]:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
import pandas as pd
|
||||
|
||||
def baz() -> pd.DataFrame:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
import pandas as pd
|
||||
|
||||
def baz() -> pd.DataFrame.Extra:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
import pandas as pd
|
||||
|
||||
def baz() -> pd.DataFrame | int:
|
||||
...
|
||||
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame
|
||||
|
||||
def baz() -> DataFrame():
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from typing import Literal
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
def baz() -> DataFrame[Literal["int"]]:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pandas import DataFrame
|
||||
|
||||
def func(value: DataFrame):
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
def baz() -> DataFrame | Series:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
def baz() -> (
|
||||
DataFrame |
|
||||
Series
|
||||
):
|
||||
...
|
||||
|
||||
class C:
|
||||
x: DataFrame[
|
||||
int
|
||||
] = 1
|
||||
|
||||
def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
|
||||
...
|
||||
2
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_future.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_future.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import __future__
|
||||
from __future__ import annotations
|
||||
4
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_lines_between.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_lines_between.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
from a import x
|
||||
import b
|
||||
from c import y
|
||||
import d
|
||||
2
crates/ruff_linter/resources/test/fixtures/isort/future_from.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/isort/future_from.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import __future__
|
||||
from __future__ import annotations
|
||||
@@ -55,3 +55,6 @@ def model_assign() -> None:
|
||||
|
||||
Bad = apps.get_model() # N806
|
||||
Bad = apps.get_model(model_name="Stream") # N806
|
||||
|
||||
Address: Type = apps.get_model("zerver", variable) # OK
|
||||
ValidationError = import_string(variable) # N806
|
||||
|
||||
@@ -63,3 +63,8 @@ for i in list(foo_tuple): # Ok
|
||||
|
||||
for i in list(foo_set): # Ok
|
||||
foo_set.append(i + 1)
|
||||
|
||||
x, y, nested_tuple = (1, 2, (3, 4, 5))
|
||||
|
||||
for i in list(nested_tuple): # PERF101
|
||||
pass
|
||||
|
||||
@@ -72,3 +72,15 @@ a = 42 # (Two spaces)
|
||||
# EF Means test is giving error and Failing
|
||||
#! Means test is segfaulting
|
||||
# 8 Means test runs forever
|
||||
|
||||
#: Colon prefix is okay
|
||||
|
||||
###This is a variable ###
|
||||
|
||||
# We should strip the space, but preserve the hashes.
|
||||
#: E266:1:3
|
||||
## Foo
|
||||
|
||||
a = 1 ## Foo
|
||||
|
||||
a = 1 #:Foo
|
||||
|
||||
@@ -60,3 +60,6 @@ def f():
|
||||
if (a and
|
||||
b):
|
||||
pass
|
||||
#: Okay
|
||||
def f():
|
||||
return 1
|
||||
|
||||
@@ -19,21 +19,32 @@ if x > 0:
|
||||
else:
|
||||
import e
|
||||
|
||||
__some__magic = 1
|
||||
import sys
|
||||
sys.path.insert(0, "some/path")
|
||||
|
||||
import f
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
|
||||
import g
|
||||
|
||||
__some__magic = 1
|
||||
|
||||
import h
|
||||
|
||||
|
||||
def foo() -> None:
|
||||
import e
|
||||
import i
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import g
|
||||
import j
|
||||
|
||||
import h; import i
|
||||
import k; import l
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import j; \
|
||||
import k
|
||||
import m; \
|
||||
import n
|
||||
|
||||
@@ -713,5 +713,12 @@ def retain_extra_whitespace_not_overindented():
|
||||
|
||||
This is not overindented
|
||||
This is overindented, but since one line is not overindented this should not raise
|
||||
And so is this, but it we should preserve the extra space on this line relative
|
||||
And so is this, but it we should preserve the extra space on this line relative
|
||||
"""
|
||||
|
||||
|
||||
def inconsistent_indent_byte_size():
|
||||
"""There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
|
||||
Returns:
|
||||
"""
|
||||
|
||||
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_20.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_20.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import re
|
||||
from typing import Annotated
|
||||
|
||||
type X = Annotated[int, lambda: re.compile("x")]
|
||||
@@ -3,6 +3,11 @@ import subprocess
|
||||
# Errors.
|
||||
subprocess.run("ls")
|
||||
subprocess.run("ls", shell=True)
|
||||
subprocess.run(
|
||||
["ls"],
|
||||
shell=False,
|
||||
)
|
||||
subprocess.run(["ls"], **kwargs)
|
||||
|
||||
# Non-errors.
|
||||
subprocess.run("ls", check=True)
|
||||
|
||||
36
crates/ruff_linter/resources/test/fixtures/pylint/too_many_locals.py
vendored
Normal file
36
crates/ruff_linter/resources/test/fixtures/pylint/too_many_locals.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
def func() -> None: # OK
|
||||
# 15 is max default
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
fourth = 4
|
||||
fifth = 5
|
||||
sixth = 6
|
||||
seventh = 7
|
||||
eighth = 8
|
||||
ninth = 9
|
||||
tenth = 10
|
||||
eleventh = 11
|
||||
twelveth = 12
|
||||
thirteenth = 13
|
||||
fourteenth = 14
|
||||
fifteenth = 15
|
||||
|
||||
|
||||
def func() -> None: # PLR0914
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
fourth = 4
|
||||
fifth = 5
|
||||
sixth = 6
|
||||
seventh = 7
|
||||
eighth = 8
|
||||
ninth = 9
|
||||
tenth = 10
|
||||
eleventh = 11
|
||||
twelfth = 12
|
||||
thirteenth = 13
|
||||
fourteenth = 14
|
||||
fifteenth = 15
|
||||
sixteenth = 16
|
||||
@@ -110,3 +110,10 @@ print('Hello %(arg)s' % bar['bop'])
|
||||
"%s" % (
|
||||
x, # comment
|
||||
)
|
||||
|
||||
|
||||
path = "%s-%s-%s.pem" % (
|
||||
safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
|
||||
cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
)
|
||||
|
||||
61
crates/ruff_linter/resources/test/fixtures/refurb/FURB118.py
vendored
Normal file
61
crates/ruff_linter/resources/test/fixtures/refurb/FURB118.py
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Errors.
|
||||
op_bitnot = lambda x: ~x
|
||||
op_not = lambda x: not x
|
||||
op_pos = lambda x: +x
|
||||
op_neg = lambda x: -x
|
||||
|
||||
op_add = lambda x, y: x + y
|
||||
op_sub = lambda x, y: x - y
|
||||
op_mult = lambda x, y: x * y
|
||||
op_matmutl = lambda x, y: x @ y
|
||||
op_truediv = lambda x, y: x / y
|
||||
op_mod = lambda x, y: x % y
|
||||
op_pow = lambda x, y: x ** y
|
||||
op_lshift = lambda x, y: x << y
|
||||
op_rshift = lambda x, y: x >> y
|
||||
op_bitor = lambda x, y: x | y
|
||||
op_xor = lambda x, y: x ^ y
|
||||
op_bitand = lambda x, y: x & y
|
||||
op_floordiv = lambda x, y: x // y
|
||||
|
||||
op_eq = lambda x, y: x == y
|
||||
op_ne = lambda x, y: x != y
|
||||
op_lt = lambda x, y: x < y
|
||||
op_lte = lambda x, y: x <= y
|
||||
op_gt = lambda x, y: x > y
|
||||
op_gte = lambda x, y: x >= y
|
||||
op_is = lambda x, y: x is y
|
||||
op_isnot = lambda x, y: x is not y
|
||||
op_in = lambda x, y: y in x
|
||||
|
||||
|
||||
def op_not2(x):
|
||||
return not x
|
||||
|
||||
|
||||
def op_add2(x, y):
|
||||
return x + y
|
||||
|
||||
|
||||
class Adder:
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
# OK.
|
||||
op_add3 = lambda x, y = 1: x + y
|
||||
op_neg2 = lambda x, y: y - x
|
||||
op_notin = lambda x, y: y not in x
|
||||
op_and = lambda x, y: y and x
|
||||
op_or = lambda x, y: y or x
|
||||
op_in = lambda x, y: x in y
|
||||
|
||||
|
||||
def op_neg3(x, y):
|
||||
return y - x
|
||||
|
||||
def op_add4(x, y = 1):
|
||||
return x + y
|
||||
|
||||
def op_add5(x, y):
|
||||
print("op_add5")
|
||||
return x + y
|
||||
@@ -5,3 +5,11 @@ A = 3.14 * r ** 2 # FURB152
|
||||
C = 6.28 * r # FURB152
|
||||
|
||||
e = 2.71 # FURB152
|
||||
|
||||
r = 3.15 # OK
|
||||
|
||||
r = 3.141 # FURB152
|
||||
|
||||
r = 3.1415 # FURB152
|
||||
|
||||
e = 2.7 # OK
|
||||
|
||||
@@ -16,6 +16,8 @@ special_log(1, 2)
|
||||
special_log(1, 10)
|
||||
special_log(1, math.e)
|
||||
special_log(1, special_e)
|
||||
math.log(1, 2.0)
|
||||
math.log(1, 10.0)
|
||||
|
||||
# Ok.
|
||||
math.log2(1)
|
||||
@@ -45,3 +47,6 @@ def log(*args):
|
||||
log(1, 2)
|
||||
log(1, 10)
|
||||
log(1, math.e)
|
||||
|
||||
math.log(1, 2.0001)
|
||||
math.log(1, 10.0001)
|
||||
|
||||
57
crates/ruff_linter/resources/test/fixtures/refurb/FURB181.py
vendored
Normal file
57
crates/ruff_linter/resources/test/fixtures/refurb/FURB181.py
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import hashlib
|
||||
from hashlib import (
|
||||
blake2b,
|
||||
blake2s,
|
||||
md5,
|
||||
sha1,
|
||||
sha3_224,
|
||||
sha3_256,
|
||||
sha3_384,
|
||||
sha3_512,
|
||||
sha224,
|
||||
)
|
||||
from hashlib import sha256
|
||||
from hashlib import sha256 as hash_algo
|
||||
from hashlib import sha384, sha512, shake_128, shake_256
|
||||
|
||||
# these will match
|
||||
|
||||
blake2b().digest().hex()
|
||||
blake2s().digest().hex()
|
||||
md5().digest().hex()
|
||||
sha1().digest().hex()
|
||||
sha224().digest().hex()
|
||||
sha256().digest().hex()
|
||||
sha384().digest().hex()
|
||||
sha3_224().digest().hex()
|
||||
sha3_256().digest().hex()
|
||||
sha3_384().digest().hex()
|
||||
sha3_512().digest().hex()
|
||||
sha512().digest().hex()
|
||||
shake_128().digest(10).hex()
|
||||
shake_256().digest(10).hex()
|
||||
|
||||
hashlib.sha256().digest().hex()
|
||||
|
||||
sha256(b"text").digest().hex()
|
||||
|
||||
hash_algo().digest().hex()
|
||||
|
||||
# not yet supported
|
||||
h = sha256()
|
||||
h.digest().hex()
|
||||
|
||||
|
||||
# these will not
|
||||
|
||||
sha256().digest()
|
||||
sha256().digest().hex("_")
|
||||
sha256().digest().hex(bytes_per_sep=4)
|
||||
sha256().hexdigest()
|
||||
|
||||
class Hash:
|
||||
def digest(self) -> bytes:
|
||||
return b""
|
||||
|
||||
|
||||
Hash().digest().hex()
|
||||
@@ -63,11 +63,29 @@ def f():
|
||||
tasks = [asyncio.create_task(task) for task in tasks]
|
||||
|
||||
|
||||
# OK (false negative)
|
||||
# Error
|
||||
def f():
|
||||
task = asyncio.create_task(coordinator.ws_connect())
|
||||
|
||||
|
||||
# Error
|
||||
def f():
|
||||
loop = asyncio.get_running_loop()
|
||||
task: asyncio.Task = loop.create_task(coordinator.ws_connect())
|
||||
|
||||
|
||||
# OK (potential false negative)
|
||||
def f():
|
||||
task = asyncio.create_task(coordinator.ws_connect())
|
||||
background_tasks.add(task)
|
||||
|
||||
|
||||
# OK
|
||||
async def f():
|
||||
task = asyncio.create_task(coordinator.ws_connect())
|
||||
await task
|
||||
|
||||
|
||||
# OK (potential false negative)
|
||||
def f():
|
||||
do_nothing_with_the_task(asyncio.create_task(coordinator.ws_connect()))
|
||||
@@ -88,3 +106,49 @@ def f():
|
||||
def f():
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.do_thing(coordinator.ws_connect())
|
||||
|
||||
|
||||
# OK
|
||||
async def f():
|
||||
task = unused = asyncio.create_task(coordinator.ws_connect())
|
||||
await task
|
||||
|
||||
|
||||
# OK (false negative)
|
||||
async def f():
|
||||
task = unused = asyncio.create_task(coordinator.ws_connect())
|
||||
|
||||
|
||||
# OK
|
||||
async def f():
|
||||
task[i] = asyncio.create_task(coordinator.ws_connect())
|
||||
|
||||
|
||||
# OK
|
||||
async def f(x: int):
|
||||
if x > 0:
|
||||
task = asyncio.create_task(make_request())
|
||||
else:
|
||||
task = asyncio.create_task(make_request())
|
||||
await task
|
||||
|
||||
|
||||
# OK
|
||||
async def f(x: bool):
|
||||
if x:
|
||||
t = asyncio.create_task(asyncio.sleep(1))
|
||||
else:
|
||||
t = None
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
finally:
|
||||
if t:
|
||||
await t
|
||||
|
||||
|
||||
# Error
|
||||
async def f(x: bool):
|
||||
if x:
|
||||
t = asyncio.create_task(asyncio.sleep(1))
|
||||
else:
|
||||
t = None
|
||||
|
||||
@@ -59,3 +59,11 @@ class F(BaseSettings):
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
|
||||
class G(F):
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_python_ast::Expr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_pie, pylint};
|
||||
use crate::rules::{flake8_pie, pylint, refurb};
|
||||
|
||||
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
|
||||
@@ -21,6 +21,9 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) {
|
||||
if checker.enabled(Rule::ReimplementedContainerBuiltin) {
|
||||
flake8_pie::rules::reimplemented_container_builtin(checker, lambda);
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &lambda.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,21 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
|
||||
use crate::rules::{
|
||||
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::AsyncioDanglingTask,
|
||||
Rule::GlobalVariableNotAssigned,
|
||||
Rule::ImportShadowedByLoopVar,
|
||||
Rule::NoSelfUse,
|
||||
Rule::RedefinedArgumentFromLocal,
|
||||
Rule::RedefinedWhileUnused,
|
||||
Rule::RuntimeImportInTypeCheckingBlock,
|
||||
Rule::TooManyLocals,
|
||||
Rule::TypingOnlyFirstPartyImport,
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Rule::TypingOnlyThirdPartyImport,
|
||||
@@ -31,7 +36,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::UnusedPrivateTypedDict,
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
Rule::UnusedVariable,
|
||||
Rule::NoSelfUse,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -59,6 +63,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
flake8_type_checking::helpers::is_valid_runtime_import(
|
||||
binding,
|
||||
&checker.semantic,
|
||||
&checker.settings.flake8_type_checking,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
@@ -268,6 +273,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::AsyncioDanglingTask) {
|
||||
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
|
||||
}
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
|
||||
@@ -335,6 +344,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NoSelfUse) {
|
||||
pylint::rules::no_self_use(checker, scope_id, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::TooManyLocals) {
|
||||
pylint::rules::too_many_locals(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.extend(diagnostics);
|
||||
|
||||
@@ -15,8 +15,9 @@ 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_trio, flake8_use_pathlib, flynt, numpy,
|
||||
pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, 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, pygrep_hooks, pylint, pyupgrade,
|
||||
refurb, ruff,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
@@ -356,6 +357,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
Rule::FString,
|
||||
// flynt
|
||||
Rule::StaticJoinToFString,
|
||||
// refurb
|
||||
Rule::HashlibDigestHex,
|
||||
]) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
||||
let attr = attr.as_str();
|
||||
@@ -543,7 +546,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryDictKwargs) {
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryRangeStart) {
|
||||
flake8_pie::rules::unnecessary_range_start(checker, call);
|
||||
@@ -581,6 +584,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::HashlibInsecureHashFunction) {
|
||||
flake8_bandit::rules::hashlib_insecure_hash_functions(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::HashlibDigestHex) {
|
||||
refurb::rules::hashlib_digest_hex(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::RequestWithoutTimeout) {
|
||||
flake8_bandit::rules::request_without_timeout(checker, call);
|
||||
}
|
||||
@@ -1165,6 +1171,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryTypeUnion) {
|
||||
flake8_pyi::rules::unnecessary_type_union(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::RuntimeStringUnion) {
|
||||
flake8_type_checking::rules::runtime_string_union(checker, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::UnaryOp(
|
||||
@@ -1270,32 +1279,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::math_constant(checker, number_literal);
|
||||
}
|
||||
}
|
||||
Expr::BytesLiteral(_) => {
|
||||
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
|
||||
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(string) => {
|
||||
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_bandit::rules::hardcoded_bind_all_interfaces(string)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedTempFile) {
|
||||
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_part in string.value.parts() {
|
||||
for string_part in value {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
|
||||
}
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::StringOrBytesTooLong) {
|
||||
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::IfExp(
|
||||
if_exp @ ast::ExprIfExp {
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(super) use module::module;
|
||||
pub(super) use parameter::parameter;
|
||||
pub(super) use parameters::parameters;
|
||||
pub(super) use statement::statement;
|
||||
pub(super) use string_like::string_like;
|
||||
pub(super) use suite::suite;
|
||||
pub(super) use unresolved_references::unresolved_references;
|
||||
|
||||
@@ -25,5 +26,6 @@ mod module;
|
||||
mod parameter;
|
||||
mod parameters;
|
||||
mod statement;
|
||||
mod string_like;
|
||||
mod suite;
|
||||
mod unresolved_references;
|
||||
|
||||
@@ -251,7 +251,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::too_many_arguments(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyPositional) {
|
||||
pylint::rules::too_many_positional(checker, parameters, stmt);
|
||||
pylint::rules::too_many_positional(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyReturnStatements) {
|
||||
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
|
||||
@@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
.diagnostics
|
||||
.extend(ruff::rules::unreachable::in_function(name, body));
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &function_def.into());
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
if checker.enabled(Rule::ReturnOutsideFunction) {
|
||||
@@ -397,27 +400,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_django::rules::nullable_model_string_field(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoExcludeWithModelForm) {
|
||||
if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(
|
||||
checker,
|
||||
arguments.as_deref(),
|
||||
body,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_django::rules::exclude_with_model_form(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoAllWithModelForm) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_django::rules::all_with_model_form(checker, arguments.as_deref(), body)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_django::rules::all_with_model_form(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) {
|
||||
flake8_django::rules::unordered_body_content_in_model(
|
||||
checker,
|
||||
arguments.as_deref(),
|
||||
body,
|
||||
);
|
||||
flake8_django::rules::unordered_body_content_in_model(checker, class_def);
|
||||
}
|
||||
if !checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
|
||||
@@ -1571,7 +1560,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::named_expr_without_context(checker, value);
|
||||
}
|
||||
if checker.enabled(Rule::AsyncioDanglingTask) {
|
||||
ruff::rules::asyncio_dangling_task(checker, value);
|
||||
if let Some(diagnostic) =
|
||||
ruff::rules::asyncio_dangling_task(value, checker.semantic())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::RepeatedAppend) {
|
||||
refurb::rules::repeated_append(checker, stmt);
|
||||
|
||||
20
crates/ruff_linter/src/checkers/ast/analyze/string_like.rs
Normal file
20
crates/ruff_linter/src/checkers/ast/analyze/string_like.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use ruff_python_ast::StringLike;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bandit, flake8_pyi};
|
||||
|
||||
/// Run lint rules over a [`StringLike`] syntax nodes.
|
||||
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
|
||||
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedTempFile) {
|
||||
flake8_bandit::rules::hardcoded_tmp_directory(checker, string_like);
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::StringOrBytesTooLong) {
|
||||
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
crates/ruff_linter/src/checkers/ast/annotation.rs
Normal file
66
crates/ruff_linter/src/checkers/ast/annotation.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
|
||||
use crate::rules::flake8_type_checking;
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum AnnotationContext {
|
||||
/// Python will evaluate the annotation at runtime, but it's not _required_ and, as such, could
|
||||
/// be quoted to convert it into a typing-only annotation.
|
||||
///
|
||||
/// For example:
|
||||
/// ```python
|
||||
/// from pandas import DataFrame
|
||||
///
|
||||
/// def foo() -> DataFrame:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Above, Python will evaluate `DataFrame` at runtime in order to add it to `__annotations__`.
|
||||
RuntimeEvaluated,
|
||||
/// Python will evaluate the annotation at runtime, and it's required to be available at
|
||||
/// runtime, as a library (like Pydantic) needs access to it.
|
||||
RuntimeRequired,
|
||||
/// The annotation is only evaluated at type-checking time.
|
||||
TypingOnly,
|
||||
}
|
||||
|
||||
impl AnnotationContext {
|
||||
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
|
||||
// If the annotation is in a class scope (e.g., an annotated assignment for a
|
||||
// class field), and that class is marked as annotation as runtime-required.
|
||||
if semantic
|
||||
.current_scope()
|
||||
.kind
|
||||
.as_class()
|
||||
.is_some_and(|class_def| {
|
||||
flake8_type_checking::helpers::runtime_required_class(
|
||||
class_def,
|
||||
&settings.flake8_type_checking.runtime_required_base_classes,
|
||||
&settings.flake8_type_checking.runtime_required_decorators,
|
||||
semantic,
|
||||
)
|
||||
})
|
||||
{
|
||||
return Self::RuntimeRequired;
|
||||
}
|
||||
|
||||
// If `__future__` annotations are enabled, then annotations are never evaluated
|
||||
// at runtime, so we can treat them as typing-only.
|
||||
if semantic.future_annotations() {
|
||||
return Self::TypingOnly;
|
||||
}
|
||||
|
||||
// Otherwise, if we're in a class or module scope, then the annotation needs to
|
||||
// be available at runtime.
|
||||
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
|
||||
if matches!(
|
||||
semantic.current_scope().kind,
|
||||
ScopeKind::Class(_) | ScopeKind::Module
|
||||
) {
|
||||
return Self::RuntimeEvaluated;
|
||||
}
|
||||
|
||||
Self::TypingOnly
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,12 @@ use ruff_python_ast::helpers::{
|
||||
};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::str::trailing_quote;
|
||||
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
|
||||
use ruff_python_ast::visitor::{walk_except_handler, walk_f_string_element, walk_pattern, Visitor};
|
||||
use ruff_python_ast::{helpers, str, visitor, PySourceType};
|
||||
use ruff_python_codegen::{Generator, Quote, Stylist};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
|
||||
use ruff_python_semantic::analyze::{typing, visibility};
|
||||
use ruff_python_semantic::analyze::{imports, typing, visibility};
|
||||
use ruff_python_semantic::{
|
||||
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
|
||||
@@ -58,6 +58,7 @@ use ruff_python_semantic::{
|
||||
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::checkers::ast::annotation::AnnotationContext;
|
||||
use crate::checkers::ast::deferred::Deferred;
|
||||
use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::importer::Importer;
|
||||
@@ -68,6 +69,7 @@ use crate::settings::{flags, LinterSettings};
|
||||
use crate::{docstrings, noqa};
|
||||
|
||||
mod analyze;
|
||||
mod annotation;
|
||||
mod deferred;
|
||||
|
||||
pub(crate) struct Checker<'a> {
|
||||
@@ -303,9 +305,12 @@ where
|
||||
}
|
||||
_ => {
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
if !self.semantic.seen_import_boundary()
|
||||
&& !helpers::is_assignment_to_a_dunder(stmt)
|
||||
&& !helpers::in_nested_block(self.semantic.current_statements())
|
||||
if !(self.semantic.seen_import_boundary()
|
||||
|| helpers::is_assignment_to_a_dunder(stmt)
|
||||
|| helpers::in_nested_block(self.semantic.current_statements())
|
||||
|| imports::is_matplotlib_activation(stmt, self.semantic())
|
||||
|| self.settings.preview.is_enabled()
|
||||
&& imports::is_sys_path_modification(stmt, self.semantic()))
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
|
||||
}
|
||||
@@ -512,8 +517,10 @@ where
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
if let Some(expr) = ¶meter_with_default.parameter.annotation {
|
||||
if runtime_annotation || singledispatch {
|
||||
self.visit_runtime_annotation(expr);
|
||||
if singledispatch {
|
||||
self.visit_runtime_required_annotation(expr);
|
||||
} else if runtime_annotation {
|
||||
self.visit_runtime_evaluated_annotation(expr);
|
||||
} else {
|
||||
self.visit_annotation(expr);
|
||||
};
|
||||
@@ -526,7 +533,7 @@ where
|
||||
if let Some(arg) = ¶meters.vararg {
|
||||
if let Some(expr) = &arg.annotation {
|
||||
if runtime_annotation {
|
||||
self.visit_runtime_annotation(expr);
|
||||
self.visit_runtime_evaluated_annotation(expr);
|
||||
} else {
|
||||
self.visit_annotation(expr);
|
||||
};
|
||||
@@ -535,7 +542,7 @@ where
|
||||
if let Some(arg) = ¶meters.kwarg {
|
||||
if let Some(expr) = &arg.annotation {
|
||||
if runtime_annotation {
|
||||
self.visit_runtime_annotation(expr);
|
||||
self.visit_runtime_evaluated_annotation(expr);
|
||||
} else {
|
||||
self.visit_annotation(expr);
|
||||
};
|
||||
@@ -543,7 +550,7 @@ where
|
||||
}
|
||||
for expr in returns {
|
||||
if runtime_annotation {
|
||||
self.visit_runtime_annotation(expr);
|
||||
self.visit_runtime_evaluated_annotation(expr);
|
||||
} else {
|
||||
self.visit_annotation(expr);
|
||||
};
|
||||
@@ -674,40 +681,16 @@ where
|
||||
value,
|
||||
..
|
||||
}) => {
|
||||
// If we're in a class or module scope, then the annotation needs to be
|
||||
// available at runtime.
|
||||
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
|
||||
let runtime_annotation = if self.semantic.future_annotations() {
|
||||
self.semantic
|
||||
.current_scope()
|
||||
.kind
|
||||
.as_class()
|
||||
.is_some_and(|class_def| {
|
||||
flake8_type_checking::helpers::runtime_evaluated_class(
|
||||
class_def,
|
||||
&self
|
||||
.settings
|
||||
.flake8_type_checking
|
||||
.runtime_evaluated_base_classes,
|
||||
&self
|
||||
.settings
|
||||
.flake8_type_checking
|
||||
.runtime_evaluated_decorators,
|
||||
&self.semantic,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
matches!(
|
||||
self.semantic.current_scope().kind,
|
||||
ScopeKind::Class(_) | ScopeKind::Module
|
||||
)
|
||||
};
|
||||
|
||||
if runtime_annotation {
|
||||
self.visit_runtime_annotation(annotation);
|
||||
} else {
|
||||
self.visit_annotation(annotation);
|
||||
match AnnotationContext::from_model(&self.semantic, self.settings) {
|
||||
AnnotationContext::RuntimeRequired => {
|
||||
self.visit_runtime_required_annotation(annotation);
|
||||
}
|
||||
AnnotationContext::RuntimeEvaluated => {
|
||||
self.visit_runtime_evaluated_annotation(annotation);
|
||||
}
|
||||
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
|
||||
}
|
||||
|
||||
if let Some(expr) = value {
|
||||
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
|
||||
self.visit_type_definition(expr);
|
||||
@@ -815,8 +798,7 @@ where
|
||||
|
||||
fn visit_expr(&mut self, expr: &'b Expr) {
|
||||
// Step 0: Pre-processing
|
||||
if !self.semantic.in_f_string()
|
||||
&& !self.semantic.in_typing_literal()
|
||||
if !self.semantic.in_typing_literal()
|
||||
&& !self.semantic.in_deferred_type_definition()
|
||||
&& self.semantic.in_type_definition()
|
||||
&& self.semantic.future_annotations()
|
||||
@@ -1238,10 +1220,7 @@ where
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
if self.semantic.in_type_definition()
|
||||
&& !self.semantic.in_typing_literal()
|
||||
&& !self.semantic.in_f_string()
|
||||
{
|
||||
if self.semantic.in_type_definition() && !self.semantic.in_typing_literal() {
|
||||
self.deferred.string_type_definitions.push((
|
||||
expr.range(),
|
||||
value.to_str(),
|
||||
@@ -1271,6 +1250,13 @@ where
|
||||
|
||||
// Step 4: Analysis
|
||||
analyze::expression(expr, self);
|
||||
match expr {
|
||||
Expr::StringLiteral(string_literal) => {
|
||||
analyze::string_like(string_literal.into(), self);
|
||||
}
|
||||
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.semantic.flags = flags_snapshot;
|
||||
self.semantic.pop_node();
|
||||
@@ -1326,17 +1312,6 @@ where
|
||||
self.semantic.flags = flags_snapshot;
|
||||
}
|
||||
|
||||
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
|
||||
match format_spec {
|
||||
Expr::FString(ast::ExprFString { value, .. }) => {
|
||||
for expr in value.elements() {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Unexpected expression for format_spec"),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_parameters(&mut self, parameters: &'b Parameters) {
|
||||
// Step 1: Binding.
|
||||
// Bind, but intentionally avoid walking default expressions, as we handle them
|
||||
@@ -1446,6 +1421,16 @@ where
|
||||
.push((bound, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_f_string_element(&mut self, f_string_element: &'b ast::FStringElement) {
|
||||
// Step 2: Traversal
|
||||
walk_f_string_element(self, f_string_element);
|
||||
|
||||
// Step 4: Analysis
|
||||
if let Some(literal) = f_string_element.as_literal() {
|
||||
analyze::string_like(literal.into(), self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
@@ -1522,10 +1507,18 @@ impl<'a> Checker<'a> {
|
||||
self.semantic.flags = snapshot;
|
||||
}
|
||||
|
||||
/// Visit an [`Expr`], and treat it as a runtime-required type annotation.
|
||||
fn visit_runtime_annotation(&mut self, expr: &'a Expr) {
|
||||
/// Visit an [`Expr`], and treat it as a runtime-evaluated type annotation.
|
||||
fn visit_runtime_evaluated_annotation(&mut self, expr: &'a Expr) {
|
||||
let snapshot = self.semantic.flags;
|
||||
self.semantic.flags |= SemanticModelFlags::RUNTIME_ANNOTATION;
|
||||
self.semantic.flags |= SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION;
|
||||
self.visit_type_definition(expr);
|
||||
self.semantic.flags = snapshot;
|
||||
}
|
||||
|
||||
/// Visit an [`Expr`], and treat it as a runtime-required type annotation.
|
||||
fn visit_runtime_required_annotation(&mut self, expr: &'a Expr) {
|
||||
let snapshot = self.semantic.flags;
|
||||
self.semantic.flags |= SemanticModelFlags::RUNTIME_REQUIRED_ANNOTATION;
|
||||
self.visit_type_definition(expr);
|
||||
self.semantic.flags = snapshot;
|
||||
}
|
||||
@@ -2020,13 +2013,15 @@ pub(crate) fn check_ast(
|
||||
// Iterate over the AST.
|
||||
checker.visit_body(python_ast);
|
||||
|
||||
// Visit any deferred syntax nodes.
|
||||
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
|
||||
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
|
||||
// function can add a deferred lambda, but the opposite is not true.
|
||||
checker.visit_deferred_functions();
|
||||
checker.visit_deferred_lambdas();
|
||||
checker.visit_deferred_future_type_definitions();
|
||||
checker.visit_deferred_type_param_definitions();
|
||||
checker.visit_deferred_future_type_definitions();
|
||||
let allocator = typed_arena::Arena::new();
|
||||
checker.visit_deferred_string_type_definitions(&allocator);
|
||||
checker.visit_deferred_lambdas();
|
||||
checker.visit_exports();
|
||||
|
||||
// Check docstrings, bindings, and unresolved references.
|
||||
|
||||
@@ -252,6 +252,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements),
|
||||
(Pylint, "R0912") => (RuleGroup::Stable, rules::pylint::rules::TooManyBranches),
|
||||
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
|
||||
(Pylint, "R0914") => (RuleGroup::Preview, rules::pylint::rules::TooManyLocals),
|
||||
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
|
||||
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
|
||||
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
|
||||
@@ -807,6 +808,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
|
||||
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||
|
||||
// tryceratops
|
||||
(Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass),
|
||||
@@ -951,6 +953,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
#[allow(deprecated)]
|
||||
@@ -965,6 +968,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
|
||||
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
|
||||
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
|
||||
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
|
||||
|
||||
// flake8-logging
|
||||
(Flake8Logging, "001") => (RuleGroup::Preview, rules::flake8_logging::rules::DirectLoggerInstantiation),
|
||||
|
||||
@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{ImportedName, SemanticModel};
|
||||
use ruff_python_trivia::textwrap::indent;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
@@ -132,7 +132,48 @@ impl<'a> Importer<'a> {
|
||||
)?;
|
||||
|
||||
// Import the `TYPE_CHECKING` symbol from the typing module.
|
||||
let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?;
|
||||
let (type_checking_edit, type_checking) =
|
||||
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
|
||||
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
|
||||
// statement that we're modifying, avoid adding a no-op edit. For example, here,
|
||||
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
|
||||
// from the import:
|
||||
// ```python
|
||||
// from __future__ import annotations
|
||||
//
|
||||
// from typing import Final, TYPE_CHECKING
|
||||
//
|
||||
// Const: Final[dict] = {}
|
||||
// ```
|
||||
let edit = if type_checking.statement(semantic) == import.statement {
|
||||
None
|
||||
} else {
|
||||
Some(Edit::range_replacement(
|
||||
self.locator.slice(type_checking.range()).to_string(),
|
||||
type_checking.range(),
|
||||
))
|
||||
};
|
||||
(edit, type_checking.into_name())
|
||||
} else {
|
||||
// Special-case: if the `TYPE_CHECKING` symbol would be added to the same import
|
||||
// we're modifying, import it as a separate import statement. For example, here,
|
||||
// we're concurrently removing `Final` and adding `TYPE_CHECKING`, so it's easier to
|
||||
// use a separate import statement:
|
||||
// ```python
|
||||
// from __future__ import annotations
|
||||
//
|
||||
// from typing import Final
|
||||
//
|
||||
// Const: Final[dict] = {}
|
||||
// ```
|
||||
let (edit, name) = self.import_symbol(
|
||||
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
|
||||
at,
|
||||
Some(import.statement),
|
||||
semantic,
|
||||
)?;
|
||||
(Some(edit), name)
|
||||
};
|
||||
|
||||
// Add the import to a `TYPE_CHECKING` block.
|
||||
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
|
||||
@@ -157,28 +198,21 @@ impl<'a> Importer<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to
|
||||
/// make the symbol available in the current scope along with the bound name of the symbol.
|
||||
fn get_or_import_type_checking(
|
||||
&self,
|
||||
/// Find a reference to `typing.TYPE_CHECKING`.
|
||||
fn find_type_checking(
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
) -> Result<Option<ImportedName>, ResolutionError> {
|
||||
for module in semantic.typing_modules() {
|
||||
if let Some((edit, name)) = self.get_symbol(
|
||||
if let Some(imported_name) = Self::find_symbol(
|
||||
&ImportRequest::import_from(module, "TYPE_CHECKING"),
|
||||
at,
|
||||
semantic,
|
||||
)? {
|
||||
return Ok((edit, name));
|
||||
return Ok(Some(imported_name));
|
||||
}
|
||||
}
|
||||
|
||||
self.import_symbol(
|
||||
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
|
||||
at,
|
||||
semantic,
|
||||
)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
|
||||
@@ -192,16 +226,15 @@ impl<'a> Importer<'a> {
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
self.get_symbol(symbol, at, semantic)?
|
||||
.map_or_else(|| self.import_symbol(symbol, at, semantic), Ok)
|
||||
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
|
||||
}
|
||||
|
||||
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn get_symbol(
|
||||
&self,
|
||||
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn find_symbol(
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<Option<(Edit, String)>, ResolutionError> {
|
||||
) -> Result<Option<ImportedName>, ResolutionError> {
|
||||
// If the symbol is already available in the current scope, use it.
|
||||
let Some(imported_name) =
|
||||
semantic.resolve_qualified_import_name(symbol.module, symbol.member)
|
||||
@@ -226,6 +259,21 @@ impl<'a> Importer<'a> {
|
||||
return Err(ResolutionError::IncompatibleContext);
|
||||
}
|
||||
|
||||
Ok(Some(imported_name))
|
||||
}
|
||||
|
||||
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn get_symbol(
|
||||
&self,
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<Option<(Edit, String)>, ResolutionError> {
|
||||
// Find the symbol in the current scope.
|
||||
let Some(imported_name) = Self::find_symbol(symbol, at, semantic)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// We also add a no-op edit to force conflicts with any other fixes that might try to
|
||||
// remove the import. Consider:
|
||||
//
|
||||
@@ -259,9 +307,13 @@ impl<'a> Importer<'a> {
|
||||
&self,
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
except: Option<&Stmt>,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
if let Some(stmt) = self.find_import_from(symbol.module, at) {
|
||||
if let Some(stmt) = self
|
||||
.find_import_from(symbol.module, at)
|
||||
.filter(|stmt| except != Some(stmt))
|
||||
{
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
@@ -423,14 +475,18 @@ impl RuntimeImportEdit {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TypingImportEdit {
|
||||
/// The edit to add the `TYPE_CHECKING` symbol to the module.
|
||||
type_checking_edit: Edit,
|
||||
type_checking_edit: Option<Edit>,
|
||||
/// The edit to add the import to a `TYPE_CHECKING` block.
|
||||
add_import_edit: Edit,
|
||||
}
|
||||
|
||||
impl TypingImportEdit {
|
||||
pub(crate) fn into_edits(self) -> Vec<Edit> {
|
||||
vec![self.type_checking_edit, self.add_import_edit]
|
||||
pub(crate) fn into_edits(self) -> (Edit, Option<Edit>) {
|
||||
if let Some(type_checking_edit) = self.type_checking_edit {
|
||||
(type_checking_edit, Some(self.add_import_edit))
|
||||
} else {
|
||||
(self.add_import_edit, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{SourceFile, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use sarif::SarifEmitter;
|
||||
pub use text::TextEmitter;
|
||||
|
||||
mod azure;
|
||||
@@ -28,6 +29,7 @@ mod json;
|
||||
mod json_lines;
|
||||
mod junit;
|
||||
mod pylint;
|
||||
mod sarif;
|
||||
mod text;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
||||
212
crates/ruff_linter/src/message/sarif.rs
Normal file
212
crates/ruff_linter/src/message/sarif.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::codes::Rule;
|
||||
use crate::fs::normalize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::{AsRule, Linter, RuleNamespace};
|
||||
use crate::VERSION;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub struct SarifEmitter;
|
||||
|
||||
impl Emitter for SarifEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
_context: &EmitterContext,
|
||||
) -> Result<()> {
|
||||
let results = messages
|
||||
.iter()
|
||||
.map(SarifResult::from_message)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let output = json!({
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"version": "2.1.0",
|
||||
"runs": [{
|
||||
"tool": {
|
||||
"driver": {
|
||||
"name": "ruff",
|
||||
"informationUri": "https://github.com/astral-sh/ruff",
|
||||
"rules": Rule::iter().map(SarifRule::from).collect::<Vec<_>>(),
|
||||
"version": VERSION.to_string(),
|
||||
}
|
||||
},
|
||||
"results": results,
|
||||
}],
|
||||
});
|
||||
serde_json::to_writer_pretty(writer, &output)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SarifRule<'a> {
|
||||
name: &'a str,
|
||||
code: String,
|
||||
linter: &'a str,
|
||||
summary: &'a str,
|
||||
explanation: Option<&'a str>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Rule> for SarifRule<'_> {
|
||||
fn from(rule: Rule) -> Self {
|
||||
let code = rule.noqa_code().to_string();
|
||||
let (linter, _) = Linter::parse_code(&code).unwrap();
|
||||
Self {
|
||||
name: rule.into(),
|
||||
code,
|
||||
linter: linter.name(),
|
||||
summary: rule.message_formats()[0],
|
||||
explanation: rule.explanation(),
|
||||
url: rule.url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SarifRule<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
json!({
|
||||
"id": self.code,
|
||||
"shortDescription": {
|
||||
"text": self.summary,
|
||||
},
|
||||
"fullDescription": {
|
||||
"text": self.explanation,
|
||||
},
|
||||
"help": {
|
||||
"text": self.summary,
|
||||
},
|
||||
"helpUri": self.url,
|
||||
"properties": {
|
||||
"id": self.code,
|
||||
"kind": self.linter,
|
||||
"name": self.name,
|
||||
"problem.severity": "error".to_string(),
|
||||
},
|
||||
})
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SarifResult {
|
||||
rule: Rule,
|
||||
level: String,
|
||||
message: String,
|
||||
uri: String,
|
||||
start_line: OneIndexed,
|
||||
start_column: OneIndexed,
|
||||
end_line: OneIndexed,
|
||||
end_column: OneIndexed,
|
||||
}
|
||||
|
||||
impl SarifResult {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn from_message(message: &Message) -> Result<Self> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(message.filename());
|
||||
Ok(Self {
|
||||
rule: message.kind.rule(),
|
||||
level: "error".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(),
|
||||
start_line: start_location.row,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.row,
|
||||
end_column: end_location.column,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn from_message(message: &Message) -> Result<Self> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(message.filename());
|
||||
Ok(Self {
|
||||
rule: message.kind.rule(),
|
||||
level: "error".to_string(),
|
||||
message: message.kind.name.clone(),
|
||||
uri: path.display().to_string(),
|
||||
start_line: start_location.row,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.row,
|
||||
end_column: end_location.column,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SarifResult {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
json!({
|
||||
"level": self.level,
|
||||
"message": {
|
||||
"text": self.message,
|
||||
},
|
||||
"locations": [{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": self.uri,
|
||||
},
|
||||
"region": {
|
||||
"startLine": self.start_line,
|
||||
"startColumn": self.start_column,
|
||||
"endLine": self.end_line,
|
||||
"endColumn": self.end_column,
|
||||
}
|
||||
}
|
||||
}],
|
||||
"ruleId": self.rule.noqa_code().to_string(),
|
||||
})
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::SarifEmitter;
|
||||
|
||||
fn get_output() -> String {
|
||||
let mut emitter = SarifEmitter {};
|
||||
capture_emitter_output(&mut emitter, &create_messages())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_json() {
|
||||
let content = get_output();
|
||||
serde_json::from_str::<serde_json::Value>(&content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_results() {
|
||||
let content = get_output();
|
||||
let sarif = serde_json::from_str::<serde_json::Value>(content.as_str()).unwrap();
|
||||
let rules = sarif["runs"][0]["tool"]["driver"]["rules"]
|
||||
.as_array()
|
||||
.unwrap();
|
||||
let results = sarif["runs"][0]["results"].as_array().unwrap();
|
||||
assert_eq!(results.len(), 3);
|
||||
assert!(rules.len() > 3);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers::{
|
||||
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
|
||||
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor, Terminal,
|
||||
};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext};
|
||||
@@ -57,6 +57,14 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
visitor.returns
|
||||
};
|
||||
|
||||
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
|
||||
let terminal = Terminal::from_function(function);
|
||||
|
||||
// If every control flow path raises an exception, return `NoReturn`.
|
||||
if terminal == Some(Terminal::Raise) {
|
||||
return Some(AutoPythonType::Never);
|
||||
}
|
||||
|
||||
// Determine the return type of the first `return` statement.
|
||||
let Some((return_statement, returns)) = returns.split_first() else {
|
||||
return Some(AutoPythonType::Atom(PythonType::None));
|
||||
@@ -80,7 +88,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
// if x > 0:
|
||||
// return 1
|
||||
// ```
|
||||
if implicit_return(function) {
|
||||
if terminal.is_none() {
|
||||
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AutoPythonType {
|
||||
Never,
|
||||
Atom(PythonType),
|
||||
Union(FxHashSet<PythonType>),
|
||||
}
|
||||
@@ -111,6 +120,28 @@ impl AutoPythonType {
|
||||
target_version: PythonVersion,
|
||||
) -> Option<(Expr, Vec<Edit>)> {
|
||||
match self {
|
||||
AutoPythonType::Never => {
|
||||
let (no_return_edit, binding) = importer
|
||||
.get_or_import_symbol(
|
||||
&ImportRequest::import_from(
|
||||
"typing",
|
||||
if target_version >= PythonVersion::Py311 {
|
||||
"Never"
|
||||
} else {
|
||||
"NoReturn"
|
||||
},
|
||||
),
|
||||
at,
|
||||
semantic,
|
||||
)
|
||||
.ok()?;
|
||||
let expr = Expr::Name(ast::ExprName {
|
||||
id: binding,
|
||||
range: TextRange::default(),
|
||||
ctx: ExprContext::Load,
|
||||
});
|
||||
Some((expr, vec![no_return_edit]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => {
|
||||
let expr = type_expr(python_type)?;
|
||||
Some((expr, vec![]))
|
||||
|
||||
@@ -537,6 +537,19 @@ fn check_dynamically_typed<F>(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_body(body: &[Stmt]) -> bool {
|
||||
body.iter().all(|stmt| match stmt {
|
||||
Stmt::Pass(_) => true,
|
||||
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
|
||||
matches!(
|
||||
value.as_ref(),
|
||||
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate flake8-annotation checks for a given `Definition`.
|
||||
pub(crate) fn definition(
|
||||
checker: &Checker,
|
||||
@@ -725,16 +738,22 @@ pub(crate) fn definition(
|
||||
) {
|
||||
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
|
||||
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
|
||||
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeClassMethod {
|
||||
name: name.to_string(),
|
||||
@@ -752,16 +771,22 @@ pub(crate) fn definition(
|
||||
}
|
||||
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
|
||||
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
|
||||
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeStaticMethod {
|
||||
name: name.to_string(),
|
||||
@@ -818,18 +843,25 @@ pub(crate) fn definition(
|
||||
match visibility {
|
||||
visibility::Visibility::Public => {
|
||||
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
});
|
||||
let return_type =
|
||||
if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
})
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeUndocumentedPublicFunction {
|
||||
name: name.to_string(),
|
||||
@@ -853,18 +885,25 @@ pub(crate) fn definition(
|
||||
}
|
||||
visibility::Visibility::Private => {
|
||||
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
});
|
||||
let return_type =
|
||||
if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
})
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypePrivateFunction {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -427,4 +427,156 @@ auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public
|
||||
148 148 | break
|
||||
149 149 | return 1
|
||||
|
||||
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
156 | class Foo(abc.ABC):
|
||||
157 | @abstractmethod
|
||||
158 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
159 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
161 | @abc.abstractmethod
|
||||
162 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
163 | """Docstring."""
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
165 | @abc.abstractmethod
|
||||
166 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
167 | ...
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
||||
|
|
||||
169 | @staticmethod
|
||||
170 | @abstractmethod
|
||||
171 | def method():
|
||||
| ^^^^^^ ANN205
|
||||
172 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
||||
|
|
||||
174 | @classmethod
|
||||
175 | @abstractmethod
|
||||
176 | def method(cls):
|
||||
| ^^^^^^ ANN206
|
||||
177 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
||||
|
|
||||
179 | @abstractmethod
|
||||
180 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
181 | if self.x > 0:
|
||||
182 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `float`
|
||||
|
||||
ℹ Unsafe fix
|
||||
177 177 | pass
|
||||
178 178 |
|
||||
179 179 | @abstractmethod
|
||||
180 |- def method(self):
|
||||
180 |+ def method(self) -> float:
|
||||
181 181 | if self.x > 0:
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
187 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
188 | try:
|
||||
189 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
184 184 | return 1.5
|
||||
185 185 |
|
||||
186 186 |
|
||||
187 |-def func(x: int):
|
||||
187 |+def func(x: int) -> int | None:
|
||||
188 188 | try:
|
||||
189 189 | pass
|
||||
190 190 | except:
|
||||
|
||||
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
194 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
195 | try:
|
||||
196 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
191 191 | return 2
|
||||
192 192 |
|
||||
193 193 |
|
||||
194 |-def func(x: int):
|
||||
194 |+def func(x: int) -> int:
|
||||
195 195 | try:
|
||||
196 196 | pass
|
||||
197 197 | except:
|
||||
|
||||
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
203 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
204 | if not x:
|
||||
205 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `Never`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import Never
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
200 201 | return 3
|
||||
201 202 |
|
||||
202 203 |
|
||||
203 |-def func(x: int):
|
||||
204 |+def func(x: int) -> Never:
|
||||
204 205 | if not x:
|
||||
205 206 | raise ValueError
|
||||
206 207 | else:
|
||||
|
||||
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
210 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
211 | if not x:
|
||||
212 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
207 207 | raise TypeError
|
||||
208 208 |
|
||||
209 209 |
|
||||
210 |-def func(x: int):
|
||||
210 |+def func(x: int) -> int:
|
||||
211 211 | if not x:
|
||||
212 212 | raise ValueError
|
||||
213 213 | else:
|
||||
|
||||
|
||||
|
||||
@@ -482,4 +482,164 @@ auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public
|
||||
148 149 | break
|
||||
149 150 | return 1
|
||||
|
||||
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
156 | class Foo(abc.ABC):
|
||||
157 | @abstractmethod
|
||||
158 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
159 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
161 | @abc.abstractmethod
|
||||
162 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
163 | """Docstring."""
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
165 | @abc.abstractmethod
|
||||
166 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
167 | ...
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
||||
|
|
||||
169 | @staticmethod
|
||||
170 | @abstractmethod
|
||||
171 | def method():
|
||||
| ^^^^^^ ANN205
|
||||
172 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
||||
|
|
||||
174 | @classmethod
|
||||
175 | @abstractmethod
|
||||
176 | def method(cls):
|
||||
| ^^^^^^ ANN206
|
||||
177 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
||||
|
|
||||
179 | @abstractmethod
|
||||
180 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
181 | if self.x > 0:
|
||||
182 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `float`
|
||||
|
||||
ℹ Unsafe fix
|
||||
177 177 | pass
|
||||
178 178 |
|
||||
179 179 | @abstractmethod
|
||||
180 |- def method(self):
|
||||
180 |+ def method(self) -> float:
|
||||
181 181 | if self.x > 0:
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
187 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
188 | try:
|
||||
189 | pass
|
||||
|
|
||||
= help: Add return type annotation: `Optional[int]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import Optional
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
184 185 | return 1.5
|
||||
185 186 |
|
||||
186 187 |
|
||||
187 |-def func(x: int):
|
||||
188 |+def func(x: int) -> Optional[int]:
|
||||
188 189 | try:
|
||||
189 190 | pass
|
||||
190 191 | except:
|
||||
|
||||
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
194 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
195 | try:
|
||||
196 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
191 191 | return 2
|
||||
192 192 |
|
||||
193 193 |
|
||||
194 |-def func(x: int):
|
||||
194 |+def func(x: int) -> int:
|
||||
195 195 | try:
|
||||
196 196 | pass
|
||||
197 197 | except:
|
||||
|
||||
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
203 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
204 | if not x:
|
||||
205 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `NoReturn`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import NoReturn
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
200 201 | return 3
|
||||
201 202 |
|
||||
202 203 |
|
||||
203 |-def func(x: int):
|
||||
204 |+def func(x: int) -> NoReturn:
|
||||
204 205 | if not x:
|
||||
205 206 | raise ValueError
|
||||
206 207 | else:
|
||||
|
||||
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
210 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
211 | if not x:
|
||||
212 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
207 207 | raise TypeError
|
||||
208 208 |
|
||||
209 209 |
|
||||
210 |-def func(x: int):
|
||||
210 |+def func(x: int) -> int:
|
||||
211 211 | if not x:
|
||||
212 212 | raise ValueError
|
||||
213 213 | else:
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::ExprStringLiteral;
|
||||
use ruff_python_ast::{self as ast, StringLike};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for hardcoded bindings to all network interfaces (`0.0.0.0`).
|
||||
@@ -34,10 +37,16 @@ impl Violation for HardcodedBindAllInterfaces {
|
||||
}
|
||||
|
||||
/// S104
|
||||
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
|
||||
if string.value.to_str() == "0.0.0.0" {
|
||||
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
|
||||
} else {
|
||||
None
|
||||
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
|
||||
let is_bind_all_interface = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
|
||||
StringLike::BytesLiteral(_) => return,
|
||||
};
|
||||
|
||||
if is_bind_all_interface {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(HardcodedBindAllInterfaces, string.range()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Violation for HardcodedSQLExpression {
|
||||
/// becomes `foobar {x}baz`.
|
||||
fn concatenated_f_string(expr: &ast::ExprFString, locator: &Locator) -> String {
|
||||
expr.value
|
||||
.parts()
|
||||
.iter()
|
||||
.filter_map(|part| {
|
||||
raw_contents(locator.slice(part)).map(|s| s.escape_default().to_string())
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_ast::{self as ast, Expr, StringLike};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -51,13 +52,19 @@ impl Violation for HardcodedTempFile {
|
||||
}
|
||||
|
||||
/// S108
|
||||
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprStringLiteral) {
|
||||
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike) {
|
||||
let value = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value,
|
||||
StringLike::BytesLiteral(_) => return,
|
||||
};
|
||||
|
||||
if !checker
|
||||
.settings
|
||||
.flake8_bandit
|
||||
.hardcoded_tmp_directory
|
||||
.iter()
|
||||
.any(|prefix| string.value.to_str().starts_with(prefix))
|
||||
.any(|prefix| value.starts_with(prefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -76,8 +83,8 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HardcodedTempFile {
|
||||
string: string.value.to_string(),
|
||||
string: value.to_string(),
|
||||
},
|
||||
string.range,
|
||||
string.range(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ S104.py:9:1: S104 Possible binding to all interfaces
|
||||
9 | "0.0.0.0"
|
||||
| ^^^^^^^^^ S104
|
||||
10 | '0.0.0.0'
|
||||
11 | f"0.0.0.0"
|
||||
|
|
||||
|
||||
S104.py:10:1: S104 Possible binding to all interfaces
|
||||
@@ -15,21 +16,30 @@ S104.py:10:1: S104 Possible binding to all interfaces
|
||||
9 | "0.0.0.0"
|
||||
10 | '0.0.0.0'
|
||||
| ^^^^^^^^^ S104
|
||||
11 | f"0.0.0.0"
|
||||
|
|
||||
|
||||
S104.py:14:6: S104 Possible binding to all interfaces
|
||||
S104.py:11:3: S104 Possible binding to all interfaces
|
||||
|
|
||||
13 | # Error
|
||||
14 | func("0.0.0.0")
|
||||
9 | "0.0.0.0"
|
||||
10 | '0.0.0.0'
|
||||
11 | f"0.0.0.0"
|
||||
| ^^^^^^^ S104
|
||||
|
|
||||
|
||||
S104.py:15:6: S104 Possible binding to all interfaces
|
||||
|
|
||||
14 | # Error
|
||||
15 | func("0.0.0.0")
|
||||
| ^^^^^^^^^ S104
|
||||
|
|
||||
|
||||
S104.py:18:9: S104 Possible binding to all interfaces
|
||||
S104.py:19:9: S104 Possible binding to all interfaces
|
||||
|
|
||||
17 | def my_func():
|
||||
18 | x = "0.0.0.0"
|
||||
18 | def my_func():
|
||||
19 | x = "0.0.0.0"
|
||||
| ^^^^^^^^^ S104
|
||||
19 | print(x)
|
||||
20 | print(x)
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -10,22 +10,31 @@ S108.py:5:11: S108 Probable insecure usage of temporary file or directory: "/tmp
|
||||
6 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:8:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
||||
S108.py:8:13: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
6 | f.write("def")
|
||||
7 |
|
||||
8 | with open("/var/tmp/123", "w") as f:
|
||||
| ^^^^^^^^^^^^^^ S108
|
||||
8 | with open(f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^ S108
|
||||
9 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
||||
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
||||
|
|
||||
9 | f.write("def")
|
||||
10 |
|
||||
11 | with open("/dev/shm/unit/test", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ S108
|
||||
11 | with open("/var/tmp/123", "w") as f:
|
||||
| ^^^^^^^^^^^^^^ S108
|
||||
12 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:14:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
||||
|
|
||||
12 | f.write("def")
|
||||
13 |
|
||||
14 | with open("/dev/shm/unit/test", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ S108
|
||||
15 | f.write("def")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -10,30 +10,39 @@ S108.py:5:11: S108 Probable insecure usage of temporary file or directory: "/tmp
|
||||
6 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:8:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
||||
S108.py:8:13: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
6 | f.write("def")
|
||||
7 |
|
||||
8 | with open("/var/tmp/123", "w") as f:
|
||||
| ^^^^^^^^^^^^^^ S108
|
||||
8 | with open(f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^ S108
|
||||
9 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
||||
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
||||
|
|
||||
9 | f.write("def")
|
||||
10 |
|
||||
11 | with open("/dev/shm/unit/test", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ S108
|
||||
11 | with open("/var/tmp/123", "w") as f:
|
||||
| ^^^^^^^^^^^^^^ S108
|
||||
12 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:15:11: S108 Probable insecure usage of temporary file or directory: "/foo/bar"
|
||||
S108.py:14:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
||||
|
|
||||
14 | # not ok by config
|
||||
15 | with open("/foo/bar", "w") as f:
|
||||
12 | f.write("def")
|
||||
13 |
|
||||
14 | with open("/dev/shm/unit/test", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ S108
|
||||
15 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:18:11: S108 Probable insecure usage of temporary file or directory: "/foo/bar"
|
||||
|
|
||||
17 | # not ok by config
|
||||
18 | with open("/foo/bar", "w") as f:
|
||||
| ^^^^^^^^^^ S108
|
||||
16 | f.write("def")
|
||||
19 | f.write("def")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
@@ -6,6 +6,7 @@ use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::add_argument;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `zip` calls without an explicit `strict` parameter.
|
||||
@@ -28,16 +29,25 @@ use crate::checkers::ast::Checker;
|
||||
/// zip(a, b, strict=True)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe for `zip` calls that contain
|
||||
/// `**kwargs`, as adding a `check` keyword argument to such a call may lead
|
||||
/// to a duplicate keyword argument error.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip)
|
||||
#[violation]
|
||||
pub struct ZipWithoutExplicitStrict;
|
||||
|
||||
impl Violation for ZipWithoutExplicitStrict {
|
||||
impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`zip()` without an explicit `strict=` parameter")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add explicit `strict=False`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// B905
|
||||
@@ -52,9 +62,27 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ZipWithoutExplicitStrict, call.range()));
|
||||
let mut diagnostic = Diagnostic::new(ZipWithoutExplicitStrict, call.range());
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
add_argument(
|
||||
"strict=False",
|
||||
&call.arguments,
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
),
|
||||
// If the function call contains `**kwargs`, mark the fix as unsafe.
|
||||
if call
|
||||
.arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.is_none())
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
B905.py:4:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
3 | # Errors
|
||||
4 | zip()
|
||||
@@ -9,8 +9,19 @@ B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
1 1 | from itertools import count, cycle, repeat
|
||||
2 2 |
|
||||
3 3 | # Errors
|
||||
4 |-zip()
|
||||
4 |+zip(strict=False)
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
|
||||
B905.py:5:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
3 | # Errors
|
||||
4 | zip()
|
||||
@@ -19,8 +30,19 @@ B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
6 | zip("a", "b")
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | # Errors
|
||||
4 4 | zip()
|
||||
5 |-zip(range(3))
|
||||
5 |+zip(range(3), strict=False)
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
|
||||
B905.py:6:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
4 | zip()
|
||||
5 | zip(range(3))
|
||||
@@ -29,8 +51,19 @@ B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
8 | zip(zip("a"), strict=False)
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
3 3 | # Errors
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 |-zip("a", "b")
|
||||
6 |+zip("a", "b", strict=False)
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
|
||||
B905.py:7:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
@@ -39,8 +72,19 @@ B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 |-zip("a", "b", *zip("c"))
|
||||
7 |+zip("a", "b", *zip("c"), strict=False)
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
|
||||
B905.py:7:16: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
@@ -49,8 +93,19 @@ B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 |-zip("a", "b", *zip("c"))
|
||||
7 |+zip("a", "b", *zip("c", strict=False))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
|
||||
B905.py:8:5: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
6 | zip("a", "b")
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
@@ -58,8 +113,19 @@ B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
|
||||
| ^^^^^^^^ B905
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 |-zip(zip("a"), strict=False)
|
||||
8 |+zip(zip("a", strict=False), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
11 11 | # OK
|
||||
|
||||
B905.py:9:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
8 | zip(zip("a"), strict=False)
|
||||
@@ -68,21 +134,49 @@ B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
10 |
|
||||
11 | # OK
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:24:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 |-zip(zip("a", strict=True))
|
||||
9 |+zip(zip("a", strict=True), strict=False)
|
||||
10 10 |
|
||||
11 11 | # OK
|
||||
12 12 | zip(range(3), strict=True)
|
||||
|
||||
B905.py:24:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
23 | # Errors (limited iterators).
|
||||
24 | zip([1, 2, 3], repeat(1, 1))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:25:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
21 21 | zip([1, 2, 3], repeat(1, times=None))
|
||||
22 22 |
|
||||
23 23 | # Errors (limited iterators).
|
||||
24 |-zip([1, 2, 3], repeat(1, 1))
|
||||
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
|
||||
25 25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
|
||||
B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
23 | # Errors (limited iterators).
|
||||
24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
23 23 | # Errors (limited iterators).
|
||||
24 24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 |-zip([1, 2, 3], repeat(1, times=4))
|
||||
25 |+zip([1, 2, 3], repeat(1, times=4), strict=False)
|
||||
|
||||
|
||||
|
||||
@@ -1083,7 +1083,7 @@ pub(crate) fn fix_unnecessary_map(
|
||||
// If the expression is embedded in an f-string, surround it with spaces to avoid
|
||||
// syntax errors.
|
||||
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
|
||||
if parent.is_some_and(Expr::is_formatted_value_expr) {
|
||||
if parent.is_some_and(Expr::is_f_string_expr) {
|
||||
content = format!(" {content} ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,21 +47,12 @@ impl Violation for DjangoAllWithModelForm {
|
||||
}
|
||||
|
||||
/// DJ007
|
||||
pub(crate) fn all_with_model_form(
|
||||
checker: &Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) -> Option<Diagnostic> {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| is_model_form(base, checker.semantic()))
|
||||
}) {
|
||||
return None;
|
||||
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for element in body {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
@@ -83,12 +73,18 @@ pub(crate) fn all_with_model_form(
|
||||
match value.as_ref() {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
if value == "__all__" {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
|
||||
if value == "__all__".as_bytes() {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
@@ -96,5 +92,4 @@ pub(crate) fn all_with_model_form(
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -46,21 +45,12 @@ impl Violation for DjangoExcludeWithModelForm {
|
||||
}
|
||||
|
||||
/// DJ006
|
||||
pub(crate) fn exclude_with_model_form(
|
||||
checker: &Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) -> Option<Diagnostic> {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| is_model_form(base, checker.semantic()))
|
||||
}) {
|
||||
return None;
|
||||
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for element in body {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
@@ -76,10 +66,12 @@ pub(crate) fn exclude_with_model_form(
|
||||
continue;
|
||||
};
|
||||
if id == "exclude" {
|
||||
return Some(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{analyze, SemanticModel};
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
|
||||
pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic.resolve_call_path(base).is_some_and(|call_path| {
|
||||
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
matches!(call_path.as_slice(), ["django", "db", "models", "Model"])
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
|
||||
pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic.resolve_call_path(base).is_some_and(|call_path| {
|
||||
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_true;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -52,57 +51,39 @@ impl Violation for DjangoModelWithoutDunderStr {
|
||||
}
|
||||
|
||||
/// DJ008
|
||||
pub(crate) fn model_without_dunder_str(
|
||||
checker: &mut Checker,
|
||||
ast::StmtClassDef {
|
||||
name,
|
||||
arguments,
|
||||
body,
|
||||
..
|
||||
}: &ast::StmtClassDef,
|
||||
) {
|
||||
if !is_non_abstract_model(arguments.as_deref(), body, checker.semantic()) {
|
||||
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_non_abstract_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
if has_dunder_method(body) {
|
||||
if has_dunder_method(class_def) {
|
||||
return;
|
||||
}
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
DjangoModelWithoutDunderStr,
|
||||
class_def.identifier(),
|
||||
));
|
||||
}
|
||||
|
||||
fn has_dunder_method(body: &[Stmt]) -> bool {
|
||||
body.iter().any(|val| match val {
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
|
||||
if name == "__str__" {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
/// Returns `true` if the class has `__str__` method.
|
||||
fn has_dunder_method(class_def: &ast::StmtClassDef) -> bool {
|
||||
class_def.body.iter().any(|val| match val {
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_non_abstract_model(
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Some(Arguments { args: bases, .. }) = arguments else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if is_model_abstract(body) {
|
||||
return false;
|
||||
/// Returns `true` if the class is a non-abstract Django model.
|
||||
fn is_non_abstract_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
if class_def.bases().is_empty() || is_model_abstract(class_def) {
|
||||
false
|
||||
} else {
|
||||
helpers::is_model(class_def, semantic)
|
||||
}
|
||||
|
||||
bases.iter().any(|base| helpers::is_model(base, semantic))
|
||||
}
|
||||
|
||||
/// Check if class is abstract, in terms of Django model inheritance.
|
||||
fn is_model_abstract(body: &[Stmt]) -> bool {
|
||||
for element in body {
|
||||
fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -79,6 +78,50 @@ impl Violation for DjangoUnorderedBodyContentInModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ012
|
||||
pub(crate) fn unordered_body_content_in_model(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
) {
|
||||
if !helpers::is_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track all the element types we've seen so far.
|
||||
let mut element_types = Vec::new();
|
||||
let mut prev_element_type = None;
|
||||
for element in &class_def.body {
|
||||
let Some(element_type) = get_element_type(element, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip consecutive elements of the same type. It's less noisy to only report
|
||||
// violations at type boundaries (e.g., avoid raising a violation for _every_
|
||||
// field declaration that's out of order).
|
||||
if prev_element_type == Some(element_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prev_element_type = Some(element_type);
|
||||
|
||||
if let Some(&prev_element_type) = element_types
|
||||
.iter()
|
||||
.find(|&&prev_element_type| prev_element_type > element_type)
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DjangoUnorderedBodyContentInModel {
|
||||
element_type,
|
||||
prev_element_type,
|
||||
},
|
||||
element.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
} else {
|
||||
element_types.push(element_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||
enum ContentType {
|
||||
FieldDeclaration,
|
||||
@@ -140,53 +183,3 @@ fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option<ContentT
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ012
|
||||
pub(crate) fn unordered_body_content_in_model(
|
||||
checker: &mut Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| helpers::is_model(base, checker.semantic()))
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track all the element types we've seen so far.
|
||||
let mut element_types = Vec::new();
|
||||
let mut prev_element_type = None;
|
||||
for element in body {
|
||||
let Some(element_type) = get_element_type(element, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip consecutive elements of the same type. It's less noisy to only report
|
||||
// violations at type boundaries (e.g., avoid raising a violation for _every_
|
||||
// field declaration that's out of order).
|
||||
if prev_element_type == Some(element_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prev_element_type = Some(element_type);
|
||||
|
||||
if let Some(&prev_element_type) = element_types
|
||||
.iter()
|
||||
.find(|&&prev_element_type| prev_element_type > element_type)
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DjangoUnorderedBodyContentInModel {
|
||||
element_type,
|
||||
prev_element_type,
|
||||
},
|
||||
element.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
} else {
|
||||
element_types.push(element_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,12 @@ DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does n
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|
||||
|
|
||||
|
||||
DJ012.py:146:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
||||
|
|
||||
144 | return "foobar"
|
||||
145 |
|
||||
146 | first_name = models.CharField(max_length=32)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -191,15 +191,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -211,15 +209,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
|
||||
if let Some(indentation) = whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -236,15 +232,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -59,15 +59,26 @@ EM.py:22:24: EM103 [*] Exception must not use a `.format()` string directly, ass
|
||||
24 25 |
|
||||
25 26 | def f_ok():
|
||||
|
||||
EM.py:32:24: EM101 Exception must not use a string literal, assign to variable first
|
||||
EM.py:32:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
30 | def f_unfixable():
|
||||
30 | def f_msg_defined():
|
||||
31 | msg = "hello"
|
||||
32 | raise RuntimeError("This is an example exception")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101
|
||||
|
|
||||
= help: Assign to variable; remove string literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 |
|
||||
30 30 | def f_msg_defined():
|
||||
31 31 | msg = "hello"
|
||||
32 |- raise RuntimeError("This is an example exception")
|
||||
32 |+ msg = "This is an example exception"
|
||||
33 |+ raise RuntimeError(msg)
|
||||
33 34 |
|
||||
34 35 |
|
||||
35 36 | def f_msg_in_nested_scope():
|
||||
|
||||
EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
37 | msg = "hello"
|
||||
@@ -88,7 +99,7 @@ EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variab
|
||||
41 42 |
|
||||
42 43 | def f_msg_in_parent_scope():
|
||||
|
||||
EM.py:46:28: EM101 Exception must not use a string literal, assign to variable first
|
||||
EM.py:46:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
45 | def nested():
|
||||
46 | raise RuntimeError("This is an example exception")
|
||||
@@ -96,6 +107,17 @@ EM.py:46:28: EM101 Exception must not use a string literal, assign to variable f
|
||||
|
|
||||
= help: Assign to variable; remove string literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
43 43 | msg = "hello"
|
||||
44 44 |
|
||||
45 45 | def nested():
|
||||
46 |- raise RuntimeError("This is an example exception")
|
||||
46 |+ msg = "This is an example exception"
|
||||
47 |+ raise RuntimeError(msg)
|
||||
47 48 |
|
||||
48 49 |
|
||||
49 50 | def f_fix_indentation_check(foo):
|
||||
|
||||
EM.py:51:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
49 | def f_fix_indentation_check(foo):
|
||||
|
||||
@@ -97,15 +97,26 @@ EM.py:22:24: EM103 [*] Exception must not use a `.format()` string directly, ass
|
||||
24 25 |
|
||||
25 26 | def f_ok():
|
||||
|
||||
EM.py:32:24: EM101 Exception must not use a string literal, assign to variable first
|
||||
EM.py:32:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
30 | def f_unfixable():
|
||||
30 | def f_msg_defined():
|
||||
31 | msg = "hello"
|
||||
32 | raise RuntimeError("This is an example exception")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101
|
||||
|
|
||||
= help: Assign to variable; remove string literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 |
|
||||
30 30 | def f_msg_defined():
|
||||
31 31 | msg = "hello"
|
||||
32 |- raise RuntimeError("This is an example exception")
|
||||
32 |+ msg = "This is an example exception"
|
||||
33 |+ raise RuntimeError(msg)
|
||||
33 34 |
|
||||
34 35 |
|
||||
35 36 | def f_msg_in_nested_scope():
|
||||
|
||||
EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
37 | msg = "hello"
|
||||
@@ -126,7 +137,7 @@ EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variab
|
||||
41 42 |
|
||||
42 43 | def f_msg_in_parent_scope():
|
||||
|
||||
EM.py:46:28: EM101 Exception must not use a string literal, assign to variable first
|
||||
EM.py:46:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
45 | def nested():
|
||||
46 | raise RuntimeError("This is an example exception")
|
||||
@@ -134,6 +145,17 @@ EM.py:46:28: EM101 Exception must not use a string literal, assign to variable f
|
||||
|
|
||||
= help: Assign to variable; remove string literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
43 43 | msg = "hello"
|
||||
44 44 |
|
||||
45 45 | def nested():
|
||||
46 |- raise RuntimeError("This is an example exception")
|
||||
46 |+ msg = "This is an example exception"
|
||||
47 |+ raise RuntimeError(msg)
|
||||
47 48 |
|
||||
48 49 |
|
||||
49 50 | def f_fix_indentation_check(foo):
|
||||
|
||||
EM.py:51:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
||||
|
|
||||
49 | def f_fix_indentation_check(foo):
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword};
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{remove_argument, Parentheses};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict` kwargs.
|
||||
@@ -40,36 +43,39 @@ use crate::checkers::ast::Checker;
|
||||
#[violation]
|
||||
pub struct UnnecessaryDictKwargs;
|
||||
|
||||
impl AlwaysFixableViolation for UnnecessaryDictKwargs {
|
||||
impl Violation for UnnecessaryDictKwargs {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary `dict` kwargs")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Remove unnecessary kwargs")
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Remove unnecessary kwargs"))
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE804
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
|
||||
for kw in kwargs {
|
||||
// keyword is a spread operator (indicated by None)
|
||||
if kw.arg.is_some() {
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let mut duplicate_keywords = None;
|
||||
for keyword in &call.arguments.keywords {
|
||||
// keyword is a spread operator (indicated by None).
|
||||
if keyword.arg.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Expr::Dict(ast::ExprDict { keys, values, .. }) = &kw.value else {
|
||||
let Expr::Dict(ast::ExprDict { keys, values, .. }) = &keyword.value else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Ex) `foo(**{**bar})`
|
||||
if matches!(keys.as_slice(), [None]) {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range());
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("**{}", checker.locator().slice(values[0].range())),
|
||||
kw.range(),
|
||||
keyword.range(),
|
||||
)));
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -86,27 +92,77 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range());
|
||||
|
||||
if values.is_empty() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(kw.start(), kw.end())));
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_argument(
|
||||
keyword,
|
||||
&call.arguments,
|
||||
Parentheses::Preserve,
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.map(Fix::safe_edit)
|
||||
});
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
kwargs
|
||||
// Compute the set of duplicate keywords (lazily).
|
||||
if duplicate_keywords.is_none() {
|
||||
duplicate_keywords = Some(duplicates(call));
|
||||
}
|
||||
|
||||
// Avoid fixing if doing so could introduce a duplicate keyword argument.
|
||||
if let Some(duplicate_keywords) = duplicate_keywords.as_ref() {
|
||||
if kwargs
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.map(|(kwarg, value)| {
|
||||
format!("{}={}", kwarg, checker.locator().slice(value.range()))
|
||||
})
|
||||
.join(", "),
|
||||
kw.range(),
|
||||
)));
|
||||
.all(|kwarg| !duplicate_keywords.contains(kwarg))
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
kwargs
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.map(|(kwarg, value)| {
|
||||
format!("{}={}", kwarg, checker.locator().slice(value.range()))
|
||||
})
|
||||
.join(", "),
|
||||
keyword.range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the set of keywords that appear in multiple positions (either directly, as in
|
||||
/// `func(x=1)`, or indirectly, as in `func(**{"x": 1})`).
|
||||
fn duplicates(call: &ast::ExprCall) -> FxHashSet<&str> {
|
||||
let mut seen = FxHashSet::with_capacity_and_hasher(
|
||||
call.arguments.keywords.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
let mut duplicates = FxHashSet::with_capacity_and_hasher(
|
||||
call.arguments.keywords.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for keyword in &call.arguments.keywords {
|
||||
if let Some(name) = &keyword.arg {
|
||||
if !seen.insert(name.as_str()) {
|
||||
duplicates.insert(name.as_str());
|
||||
}
|
||||
} else if let Expr::Dict(ast::ExprDict { keys, .. }) = &keyword.value {
|
||||
for key in keys {
|
||||
if let Some(name) = key.as_ref().and_then(as_kwarg) {
|
||||
if !seen.insert(name) {
|
||||
duplicates.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
duplicates
|
||||
}
|
||||
|
||||
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
|
||||
fn as_kwarg(key: &Expr) -> Option<&str> {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pie/mod.rs
|
||||
---
|
||||
PIE804.py:1:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:1:5: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
1 | foo(**{"bar": True}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
| ^^^^^^^^^^^^^^^ PIE804
|
||||
2 |
|
||||
3 | foo(**{"r2d2": True}) # PIE804
|
||||
|
|
||||
@@ -17,12 +17,12 @@ PIE804.py:1:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
3 3 | foo(**{"r2d2": True}) # PIE804
|
||||
4 4 |
|
||||
|
||||
PIE804.py:3:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:3:5: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
1 | foo(**{"bar": True}) # PIE804
|
||||
2 |
|
||||
3 | foo(**{"r2d2": True}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
| ^^^^^^^^^^^^^^^^ PIE804
|
||||
4 |
|
||||
5 | Foo.objects.create(**{"bar": True}) # PIE804
|
||||
|
|
||||
@@ -37,12 +37,12 @@ PIE804.py:3:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
5 5 | Foo.objects.create(**{"bar": True}) # PIE804
|
||||
6 6 |
|
||||
|
||||
PIE804.py:5:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:5:20: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
3 | foo(**{"r2d2": True}) # PIE804
|
||||
4 |
|
||||
5 | Foo.objects.create(**{"bar": True}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
| ^^^^^^^^^^^^^^^ PIE804
|
||||
6 |
|
||||
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
|
||||
|
|
||||
@@ -58,12 +58,12 @@ PIE804.py:5:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
7 7 | Foo.objects.create(**{"_id": some_id}) # PIE804
|
||||
8 8 |
|
||||
|
||||
PIE804.py:7:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:7:20: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
5 | Foo.objects.create(**{"bar": True}) # PIE804
|
||||
6 |
|
||||
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^ PIE804
|
||||
8 |
|
||||
9 | Foo.objects.create(**{**bar}) # PIE804
|
||||
|
|
||||
@@ -79,12 +79,12 @@ PIE804.py:7:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
9 9 | Foo.objects.create(**{**bar}) # PIE804
|
||||
10 10 |
|
||||
|
||||
PIE804.py:9:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:9:20: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
|
||||
8 |
|
||||
9 | Foo.objects.create(**{**bar}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
| ^^^^^^^^^ PIE804
|
||||
10 |
|
||||
11 | foo(**{})
|
||||
|
|
||||
@@ -100,12 +100,14 @@ PIE804.py:9:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
11 11 | foo(**{})
|
||||
12 12 |
|
||||
|
||||
PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
PIE804.py:11:5: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
9 | Foo.objects.create(**{**bar}) # PIE804
|
||||
10 |
|
||||
11 | foo(**{})
|
||||
| ^^^^^^^^^ PIE804
|
||||
| ^^^^ PIE804
|
||||
12 |
|
||||
13 | foo(**{**data, "foo": "buzz"})
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
@@ -116,7 +118,71 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
11 |-foo(**{})
|
||||
11 |+foo()
|
||||
12 12 |
|
||||
13 13 |
|
||||
14 14 | foo(**{**data, "foo": "buzz"})
|
||||
13 13 | foo(**{**data, "foo": "buzz"})
|
||||
14 14 | foo(**buzz)
|
||||
|
||||
PIE804.py:22:5: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
20 | foo(**{f"buzz__{bar}": True})
|
||||
21 | abc(**{"for": 3})
|
||||
22 | foo(**{},)
|
||||
| ^^^^ PIE804
|
||||
23 |
|
||||
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 | foo(**{"": True})
|
||||
20 20 | foo(**{f"buzz__{bar}": True})
|
||||
21 21 | abc(**{"for": 3})
|
||||
22 |-foo(**{},)
|
||||
22 |+foo()
|
||||
23 23 |
|
||||
24 24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
|
||||
PIE804.py:25:5: PIE804 Unnecessary `dict` kwargs
|
||||
|
|
||||
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
PIE804.py:25:17: PIE804 Unnecessary `dict` kwargs
|
||||
|
|
||||
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
PIE804.py:26:10: PIE804 Unnecessary `dict` kwargs
|
||||
|
|
||||
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | # Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
25 25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 |-abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
26 |+abc(a=1, **{'a': c}, b=c) # PIE804
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::delete_stmt;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
@@ -28,14 +28,24 @@ use crate::registry::AsRule;
|
||||
/// def add_numbers(a, b):
|
||||
/// return a + b
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may remove `print` statements
|
||||
/// that are used beyond debugging purposes.
|
||||
#[violation]
|
||||
pub struct Print;
|
||||
|
||||
impl Violation for Print {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`print` found")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `print`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -65,19 +75,29 @@ impl Violation for Print {
|
||||
/// dict_c = {**dict_a, **dict_b}
|
||||
/// return dict_c
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may remove `pprint` statements
|
||||
/// that are used beyond debugging purposes.
|
||||
#[violation]
|
||||
pub struct PPrint;
|
||||
|
||||
impl Violation for PPrint {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`pprint` found")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `pprint`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// T201, T203
|
||||
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let diagnostic = {
|
||||
let mut diagnostic = {
|
||||
let call_path = checker.semantic().resolve_call_path(&call.func);
|
||||
if call_path
|
||||
.as_ref()
|
||||
@@ -113,5 +133,15 @@ pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the `print`, if it's a standalone statement.
|
||||
if checker.semantic().current_expression_parent().is_none() {
|
||||
let statement = checker.semantic().current_statement();
|
||||
let parent = checker.semantic().current_statement_parent();
|
||||
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_parent_id(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
|
||||
---
|
||||
T201.py:4:1: T201 `print` found
|
||||
T201.py:4:1: T201 [*] `print` found
|
||||
|
|
||||
2 | import tempfile
|
||||
3 |
|
||||
@@ -10,8 +10,18 @@ T201.py:4:1: T201 `print` found
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:5:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
1 1 | import sys
|
||||
2 2 | import tempfile
|
||||
3 3 |
|
||||
4 |-print("Hello, world!") # T201
|
||||
5 4 | print("Hello, world!", file=None) # T201
|
||||
6 5 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
||||
T201.py:5:1: T201 [*] `print` found
|
||||
|
|
||||
4 | print("Hello, world!") # T201
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
@@ -19,8 +29,18 @@ T201.py:5:1: T201 `print` found
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:6:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
2 2 | import tempfile
|
||||
3 3 |
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 |-print("Hello, world!", file=None) # T201
|
||||
6 5 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
|
||||
T201.py:6:1: T201 [*] `print` found
|
||||
|
|
||||
4 | print("Hello, world!") # T201
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
@@ -28,8 +48,18 @@ T201.py:6:1: T201 `print` found
|
||||
| ^^^^^ T201
|
||||
7 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:7:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
3 3 |
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 5 | print("Hello, world!", file=None) # T201
|
||||
6 |-print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
9 8 | with tempfile.NamedTemporaryFile() as fp:
|
||||
|
||||
T201.py:7:1: T201 [*] `print` found
|
||||
|
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
@@ -38,5 +68,15 @@ T201.py:7:1: T201 `print` found
|
||||
8 |
|
||||
9 | with tempfile.NamedTemporaryFile() as fp:
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 5 | print("Hello, world!", file=None) # T201
|
||||
6 6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 |-print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
9 8 | with tempfile.NamedTemporaryFile() as fp:
|
||||
10 9 | print("Hello, world!", file=fp) # OK
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
|
||||
---
|
||||
T203.py:3:1: T203 `pprint` found
|
||||
T203.py:3:1: T203 [*] `pprint` found
|
||||
|
|
||||
1 | from pprint import pprint
|
||||
2 |
|
||||
@@ -10,8 +10,17 @@ T203.py:3:1: T203 `pprint` found
|
||||
4 |
|
||||
5 | import pprint
|
||||
|
|
||||
= help: Remove `pprint`
|
||||
|
||||
T203.py:7:1: T203 `pprint` found
|
||||
ℹ Unsafe fix
|
||||
1 1 | from pprint import pprint
|
||||
2 2 |
|
||||
3 |-pprint("Hello, world!") # T203
|
||||
4 3 |
|
||||
5 4 | import pprint
|
||||
6 5 |
|
||||
|
||||
T203.py:7:1: T203 [*] `pprint` found
|
||||
|
|
||||
5 | import pprint
|
||||
6 |
|
||||
@@ -20,5 +29,14 @@ T203.py:7:1: T203 `pprint` found
|
||||
8 |
|
||||
9 | pprint.pformat("Hello, world!")
|
||||
|
|
||||
= help: Remove `pprint`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 |
|
||||
5 5 | import pprint
|
||||
6 6 |
|
||||
7 |-pprint.pprint("Hello, world!") # T203
|
||||
8 7 |
|
||||
9 8 | pprint.pformat("Hello, world!")
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::{self as ast, StringLike};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -44,25 +43,27 @@ impl AlwaysFixableViolation for StringOrBytesTooLong {
|
||||
}
|
||||
|
||||
/// PYI053
|
||||
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) {
|
||||
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike) {
|
||||
// Ignore docstrings.
|
||||
if is_docstring_stmt(checker.semantic().current_statement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let length = match expr {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
|
||||
_ => return,
|
||||
let length = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
|
||||
StringLike::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
|
||||
value.chars().count()
|
||||
}
|
||||
};
|
||||
if length <= 50 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, string.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"...".to_string(),
|
||||
expr.range(),
|
||||
string.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -80,17 +80,24 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
|
||||
}
|
||||
|
||||
let mut type_exprs = Vec::new();
|
||||
let mut other_exprs = Vec::new();
|
||||
|
||||
let mut collect_type_exprs = |expr: &'a Expr, _| {
|
||||
let Some(subscript) = expr.as_subscript_expr() else {
|
||||
return;
|
||||
};
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(subscript.value.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
|
||||
{
|
||||
type_exprs.push(&subscript.slice);
|
||||
let subscript = expr.as_subscript_expr();
|
||||
|
||||
if subscript.is_none() {
|
||||
other_exprs.push(expr);
|
||||
} else {
|
||||
let unwrapped = subscript.unwrap();
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(unwrapped.value.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
|
||||
{
|
||||
type_exprs.push(&unwrapped.slice);
|
||||
} else {
|
||||
other_exprs.push(expr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,55 +120,82 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
|
||||
|
||||
if checker.semantic().is_builtin("type") {
|
||||
let content = if let Some(subscript) = subscript {
|
||||
checker
|
||||
.generator()
|
||||
.expr(&Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: type_members
|
||||
.into_iter()
|
||||
.map(|type_member| {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: type_member,
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
}))
|
||||
} else {
|
||||
checker
|
||||
.generator()
|
||||
.expr(&Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(concatenate_bin_ors(
|
||||
type_exprs
|
||||
.clone()
|
||||
})),
|
||||
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: type_members
|
||||
.into_iter()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.map(|type_member| {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: type_member,
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
}))
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
if other_exprs.is_empty() {
|
||||
checker.generator().expr(types)
|
||||
} else {
|
||||
let mut exprs = Vec::new();
|
||||
exprs.push(types);
|
||||
exprs.extend(other_exprs);
|
||||
|
||||
let union = Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: exprs.into_iter().cloned().collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
checker.generator().expr(&union)
|
||||
}
|
||||
} else {
|
||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(concatenate_bin_ors(
|
||||
type_exprs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.collect(),
|
||||
)),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
if other_exprs.is_empty() {
|
||||
checker.generator().expr(types)
|
||||
} else {
|
||||
let mut exprs = Vec::new();
|
||||
exprs.push(types);
|
||||
exprs.extend(other_exprs);
|
||||
|
||||
checker.generator().expr(&concatenate_bin_ors(exprs))
|
||||
}
|
||||
};
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
|
||||
@@ -7,28 +7,35 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of unused private `TypeVar` declarations.
|
||||
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
|
||||
/// `TypeVarTuple` declarations.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake, and
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake. It
|
||||
/// should either be used, made public, or removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import typing
|
||||
/// import typing_extensions
|
||||
///
|
||||
/// _T = typing.TypeVar("_T")
|
||||
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UnusedPrivateTypeVar {
|
||||
name: String,
|
||||
type_var_like_name: String,
|
||||
type_var_like_kind: String,
|
||||
}
|
||||
|
||||
impl Violation for UnusedPrivateTypeVar {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnusedPrivateTypeVar { name } = self;
|
||||
format!("Private TypeVar `{name}` is never used")
|
||||
let UnusedPrivateTypeVar {
|
||||
type_var_like_name,
|
||||
type_var_like_kind,
|
||||
} = self;
|
||||
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +192,26 @@ pub(crate) fn unused_private_type_var(
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if !checker.semantic().match_typing_expr(func, "TypeVar") {
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let Some(type_var_like_kind) = semantic.resolve_call_path(func).and_then(|call_path| {
|
||||
if semantic.match_typing_call_path(&call_path, "TypeVar") {
|
||||
Some("TypeVar")
|
||||
} else if semantic.match_typing_call_path(&call_path, "ParamSpec") {
|
||||
Some("ParamSpec")
|
||||
} else if semantic.match_typing_call_path(&call_path, "TypeVarTuple") {
|
||||
Some("TypeVarTuple")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
diagnostics.push(Diagnostic::new(
|
||||
UnusedPrivateTypeVar {
|
||||
name: id.to_string(),
|
||||
type_var_like_name: id.to_string(),
|
||||
type_var_like_kind: type_var_like_kind.to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user