Compare commits

...

28 Commits

Author SHA1 Message Date
Charlie Marsh
9b07d0bd92 Bump version to 0.0.234 2023-01-25 16:55:57 -05:00
Charlie Marsh
23525a8ea0 Actually, rename TYP rules to TCH (#2176) 2023-01-25 16:52:49 -05:00
Charlie Marsh
6ede030700 Allow manual releases for pre-release testing 2023-01-25 16:37:04 -05:00
Charlie Marsh
44f3e5013d Add flake8-type-checking license 2023-01-25 16:27:18 -05:00
Charlie Marsh
35cf9e242e Rename TYP rules to TYC (#2175) 2023-01-25 16:26:22 -05:00
Charlie Marsh
d5dff11d4b Avoid reraise-no-cause for explicit reraises (#2174) 2023-01-25 15:51:24 -05:00
Charlie Marsh
8e1fac620e Add flake8-builtins options to README (#2173) 2023-01-25 15:43:26 -05:00
Aarni Koskela
0da691c0d5 Add Babel to readme (#2170) 2023-01-25 15:21:26 -05:00
Hugo van Kemenade
233415921b Add colour to CI for readability 2023-01-25 15:21:10 -05:00
Hugo van Kemenade
81141e2a73 Bump GitHub Actions 2023-01-25 15:21:10 -05:00
Hugo van Kemenade
6e255ad53c Allow testing feature branches 2023-01-25 15:21:10 -05:00
Hugo van Kemenade
6d87adbcc0 Fix singular and plural for error(s) 2023-01-25 15:21:10 -05:00
Florian Best
43a8ce6c89 fix: avoid flagging unused loop variable (B007) with globals(), vars() or eval() (#2166) 2023-01-25 15:18:58 -05:00
Charlie Marsh
a978706dce Re-add error wrapper in main.rs (#2168) 2023-01-25 15:11:24 -05:00
Florian Best
dc1aa8dd1d Suggest input format in error case (#2167) 2023-01-25 14:55:04 -05:00
Charlie Marsh
662e29b1ce Avoid re-resolving settings for repeated paths (#2165)
After this change:

```shell
> time cargo run -- -n $(find ../django -type f -name '*.py')`
8.85s user 0.20s system 498% cpu 1.814 total
> time cargo run -- -n ../django
8.95s user 0.23s system 507% cpu 1.811 total
```

I also verified that we only hit the creation path once via some manual logging.

Closes #2154.
2023-01-25 13:38:33 -05:00
Charlie Marsh
6978dcf035 Add an FAQ on autofix (#2163) 2023-01-25 13:09:16 -05:00
Charlie Marsh
0e6f513607 Avoid prefer-type-error (TRY004) with intermediary control flow (#2162) 2023-01-25 13:00:59 -05:00
Charlie Marsh
02421d02f5 Avoid flagging unused loop variable (B007) with locals() (#2161) 2023-01-25 12:53:35 -05:00
Charlie Marsh
38de46ae3c Treat Python 3.7 as minimum supported version (#2159) 2023-01-25 12:36:50 -05:00
Martin Fischer
f6fd702d41 Add #![warn(clippy::pedantic)] to lib.rs and main.rs files
We already enforced pedantic clippy lints via the
following command in .github/workflows/ci.yaml:

    cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic

Additionally adding #![warn(clippy::pedantic)] to all main.rs and lib.rs
has the benefit that violations of pedantic clippy lints are also
reported when just running `cargo clippy` without any arguments and
are thereby also picked up by LSP[1] servers such as rust-analyzer[2].
However for rust-analyzer to run clippy you'll have to configure:

    "rust-analyzer.check.command": "clippy",

in your editor.[3]

[1]: https://microsoft.github.io/language-server-protocol/
[2]: https://rust-analyzer.github.io/
[3]: https://rust-analyzer.github.io/manual.html#configuration
2023-01-25 00:40:29 -05:00
Martin Fischer
2125d0bb54 refactor: Move #![forbid(unsafe_code)] attributes up
What's forbidden is more important than which clippy lints are
ignored and more important directives should come first.
2023-01-25 00:40:29 -05:00
Charlie Marsh
63b4f60ba4 Implement typing-only import detection (TYP001, TYP002, TYP003) (#2147) 2023-01-24 23:48:11 -05:00
Charlie Marsh
9eb13bc9da Downgrade recommended pre-commit version to v0.0.231 2023-01-24 23:47:13 -05:00
Charlie Marsh
0758049e49 Implement runtime-import-in-type-checking-block (TYP004) (#2146) 2023-01-24 23:33:26 -05:00
Charlie Marsh
deff503932 Avoid generating dirty call paths (#2144) 2023-01-24 20:40:38 -05:00
Jonathan Plasse
82d7814101 Update .pre-commit-config.yml (#2139) 2023-01-24 19:45:34 -05:00
Eric Roberts
0cac1a0d21 Move is_overlong to helpers (#2137) 2023-01-24 12:45:35 -05:00
94 changed files with 1608 additions and 264 deletions

View File

@@ -1,10 +1,6 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
on: [push, pull_request, workflow_dispatch]
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
@@ -13,6 +9,7 @@ concurrency:
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
@@ -153,7 +150,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Check spelling of file.txt
uses: crate-ci/typos@master

View File

@@ -12,6 +12,7 @@ env:
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
@@ -38,7 +39,7 @@ jobs:
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -64,7 +65,7 @@ jobs:
run: |
pip install dist/${{ env.CRATE_NAME }}-*universal2.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -96,7 +97,7 @@ jobs:
run: |
python -m pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -123,7 +124,7 @@ jobs:
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -144,7 +145,7 @@ jobs:
target: ${{ matrix.target }}
manylinux: auto
args: --no-default-features --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- uses: uraimo/run-on-arch-action@v2.0.5
- uses: uraimo/run-on-arch-action@v2.5.0
if: matrix.target != 'ppc64'
name: Install built wheel
with:
@@ -158,7 +159,7 @@ jobs:
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -192,7 +193,7 @@ jobs:
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -228,7 +229,7 @@ jobs:
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -262,7 +263,7 @@ jobs:
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -280,7 +281,7 @@ jobs:
- musllinux-cross
- pypy
steps:
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: wheels
- uses: actions/setup-python@v4

View File

@@ -8,6 +8,7 @@ on:
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:

View File

@@ -1,6 +1,7 @@
name: "[ruff] Release"
on:
workflow_dispatch:
release:
types: [published]
@@ -13,6 +14,7 @@ env:
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
@@ -39,7 +41,7 @@ jobs:
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -65,7 +67,7 @@ jobs:
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -97,7 +99,7 @@ jobs:
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -124,7 +126,7 @@ jobs:
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -145,7 +147,7 @@ jobs:
target: ${{ matrix.target }}
manylinux: auto
args: --no-default-features --release --out dist
- uses: uraimo/run-on-arch-action@v2.0.5
- uses: uraimo/run-on-arch-action@v2.5.0
if: matrix.target != 'ppc64'
name: Install built wheel
with:
@@ -159,7 +161,7 @@ jobs:
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -193,7 +195,7 @@ jobs:
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -229,7 +231,7 @@ jobs:
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -263,7 +265,7 @@ jobs:
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -282,7 +284,7 @@ jobs:
- pypy
if: "startsWith(github.ref, 'refs/tags/')"
steps:
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: wheels
- uses: actions/setup-python@v4

View File

@@ -1,8 +1,10 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.233
rev: v0.0.234
hooks:
- id: ruff
args: [--fix]
exclude: ^resources
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.10.1
@@ -13,6 +15,6 @@ repos:
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --
entry: cargo +nightly fmt --
language: rust
types: [rust]

View File

@@ -46,7 +46,7 @@ For rustfmt and Clippy, we use [nightly Rust][nightly], as it is stricter than s
```shell
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --fix --workspace --all-targets --all-features -- -W clippy::pedantic # Linting...
cargo +nightly clippy --fix --workspace --all-targets --all-features # Linting...
cargo test --all # Testing...
```

10
Cargo.lock generated
View File

@@ -719,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1837,7 +1837,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"anyhow",
"bitflags",
@@ -1892,7 +1892,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1929,7 +1929,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1950,7 +1950,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"once_cell",
"proc-macro2",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.233"
version = "0.0.234"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,7 +46,7 @@ num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.233", path = "ruff_macros" }
ruff_macros = { version = "0.0.234", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }

View File

@@ -67,6 +67,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Home Assistant](https://github.com/home-assistant/core)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel)
- [Babel](https://github.com/python-babel/babel)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -143,7 +144,7 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [flake8-commas (COM)](#flake8-commas-com)
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-type-checking (TYP)](#flake8-type-checking-typ)
1. [flake8-type-checking (TCH)](#flake8-type-checking-tch)
1. [tryceratops (TRY)](#tryceratops-try)
1. [flake8-use-pathlib (PTH)](#flake8-use-pathlib-pth)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
@@ -212,7 +213,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.233'
rev: 'v0.0.234'
hooks:
- id: ruff
```
@@ -853,7 +854,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) on PyPI
| B004 | unreliable-callable-check | Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results. | |
| B005 | strip-with-multi-characters | Using `.strip()` with multi-character strings is misleading the reader | |
| B006 | mutable-argument-default | Do not use mutable data structures for argument defaults | |
| B007 | unused-loop-control-variable | Loop control variable `{name}` not used within the loop body | 🛠 |
| B007 | unused-loop-control-variable | Loop control variable `{name}` not used within loop body | |
| B008 | function-call-argument-default | Do not perform function call `{name}` in argument defaults | |
| B009 | get-attr-with-constant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | set-attr-with-constant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
@@ -1196,13 +1197,17 @@ For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) o
| EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 |
| EXE005 | shebang-newline | Shebang should be at the beginning of the file | |
### flake8-type-checking (TYP)
### flake8-type-checking (TCH)
For more, see [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TYP005 | empty-type-checking-block | Found empty type-checking block | |
| TCH001 | typing-only-first-party-import | Move application import `{}` into a type-checking block | |
| TCH002 | typing-only-third-party-import | Move third-party import `{}` into a type-checking block | |
| TCH003 | typing-only-standard-library-import | Move standard library import `{}` into a type-checking block | |
| TCH004 | runtime-import-in-type-checking-block | Move import `{}` out of type-checking block. Import is used for more than type hinting. | |
| TCH005 | empty-type-checking-block | Found empty type-checking block | |
### tryceratops (TRY)
@@ -1692,7 +1697,7 @@ After installing `ruff` and `nbqa`, you can run Ruff over a notebook like so:
Untitled.ipynb:cell_1:2:5: F841 Local variable `x` is assigned to but never used
Untitled.ipynb:cell_2:1:1: E402 Module level import not at top of file
Untitled.ipynb:cell_2:1:8: F401 `os` imported but unused
Found 3 error(s).
Found 3 errors.
1 potentially fixable with the --fix option.
```
@@ -1728,6 +1733,25 @@ matter how they're provided, which avoids accidental incompatibilities and simpl
Run `ruff /path/to/code.py --show-settings` to view the resolved settings for a given file.
### Ruff tried to fix something, but it broke my code. What should I do?
Ruff's autofix is a best-effort mechanism. Given the dynamic nature of Python, it's difficult to
have _complete_ certainty when making changes to code, even for the seemingly trivial fixes.
In the future, Ruff will support enabling autofix behavior based on the safety of the patch.
In the meantime, if you find that the autofix is too aggressive, you can disable it on a per-rule or
per-category basis using the [`unfixable`](#unfixable) mechanic. For example, to disable autofix
for some possibly-unsafe rules, you could add the following to your `pyproject.toml`:
```toml
[tool.ruff]
unfixable = ["B", "SIM", "TRY", "RUF"]
```
If you find a case where Ruff's autofix breaks your code, please file an Issue!
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
@@ -2677,6 +2701,25 @@ extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
---
### `flake8-builtins`
#### [`builtins-ignorelist`](#builtins-ignorelist)
Ignore list of builtins.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.flake8-builtins]
builtins-ignorelist = ["id"]
```
---
### `flake8-errmsg`
#### [`max-string-length`](#max-string-length)

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.233"
version = "0.0.234"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.233"
version = "0.0.234"
edition = "2021"
[dependencies]

View File

@@ -1,4 +1,6 @@
//! Utility to generate Ruff's `pyproject.toml` section from a Flake8 INI file.
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -11,7 +13,6 @@
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
use std::path::PathBuf;

View File

@@ -0,0 +1,27 @@
Copyright (c) 2021, Sondre Lillebø Gundersen
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of pytest-{{ cookiecutter.plugin_name }} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.233"
version = "0.0.234"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -29,3 +29,19 @@ def strange_generator():
for i, (j, (k, l)) in strange_generator(): # i, k not used
print(j, l)
FMT = "{foo} {bar}"
for foo, bar in [(1, 2)]:
if foo:
print(FMT.format(**locals()))
for foo, bar in [(1, 2)]:
if foo:
print(FMT.format(**globals()))
for foo, bar in [(1, 2)]:
if foo:
print(FMT.format(**vars()))
for foo, bar in [(1, 2)]:
print(FMT.format(foo=foo, bar=eval('bar')))

View File

@@ -0,0 +1,28 @@
"""Tests to determine first-party import classification.
For typing-only import detection tests, see `TCH002.py`.
"""
def f():
import TYP001
x: TYP001
def f():
import TYP001
print(TYP001)
def f():
from . import TYP001
x: TYP001
def f():
from . import TYP001
print(TYP001)

View File

@@ -0,0 +1,148 @@
"""Tests to determine accurate detection of typing-only imports."""
def f():
import pandas as pd # TCH002
x: pd.DataFrame
def f():
from pandas import DataFrame # TCH002
x: DataFrame
def f():
from pandas import DataFrame as df # TCH002
x: df
def f():
import pandas as pd # TCH002
x: pd.DataFrame = 1
def f():
from pandas import DataFrame # TCH002
x: DataFrame = 2
def f():
from pandas import DataFrame as df # TCH002
x: df = 3
def f():
import pandas as pd # TCH002
x: "pd.DataFrame" = 1
def f():
import pandas as pd
x = dict["pd.DataFrame", "pd.DataFrame"] # TCH002
def f():
import pandas as pd
print(pd)
def f():
from pandas import DataFrame
print(DataFrame)
def f():
from pandas import DataFrame
def f():
print(DataFrame)
def f():
from typing import Dict, Any
def example() -> Any:
return 1
x: Dict[int] = 20
def f():
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Dict
x: Dict[int] = 20
def f():
from pathlib import Path
class ImportVisitor(ast.NodeTransformer):
def __init__(self, cwd: Path) -> None:
self.cwd = cwd
origin = Path(spec.origin)
class ExampleClass:
def __init__(self):
self.cwd = Path(pandas.getcwd())
def f():
import pandas
class Migration:
enum = pandas
def f():
import pandas
class Migration:
enum = pandas.EnumClass
def f():
from typing import TYPE_CHECKING
from pandas import y
if TYPE_CHECKING:
_type = x
else:
_type = y
def f():
from typing import TYPE_CHECKING
from pandas import y
if TYPE_CHECKING:
_type = x
elif True:
_type = y
def f():
from typing import cast
import pandas as pd
x = cast(pd.DataFrame, 2)
def f():
import pandas as pd
x = dict[pd.DataFrame, pd.DataFrame]

View File

@@ -0,0 +1,16 @@
"""Tests to determine standard library import classification.
For typing-only import detection tests, see `TCH002.py`.
"""
def f():
import os
x: os
def f():
import os
print(os)

View File

@@ -0,0 +1,5 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import datetime
x = datetime

View File

@@ -0,0 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import date
def example():
return date()

View File

@@ -0,0 +1,6 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
CustomType = Any

View File

@@ -0,0 +1,11 @@
from typing import TYPE_CHECKING, Type
if TYPE_CHECKING:
from typing import Any
def example(*args: Any, **kwargs: Any):
return
my_type: Type[Any] | Any

View File

@@ -0,0 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Sequence, Set
def example(a: List[int], /, b: Sequence[int], *, c: Set[int]):
return

View File

@@ -0,0 +1,10 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pandas import DataFrame
def example():
from pandas import DataFrame
x = DataFrame

View File

@@ -0,0 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import AsyncIterator, List
class Example:
async def example(self) -> AsyncIterator[List[str]]:
yield 0

View File

@@ -0,0 +1,7 @@
from typing import TYPE_CHECKING
from weakref import WeakKeyDictionary
if TYPE_CHECKING:
from typing import Any
d = WeakKeyDictionary["Any", "Any"]()

View File

@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
pass # TYP005
pass # TCH005
def example():

View File

@@ -0,0 +1,3 @@
from pathlib import Path
(Path("") / "").open()

View File

@@ -94,7 +94,7 @@ def incorrect_MemoryError(some_arg):
# not multiline is on purpose for fix
raise MemoryError(
"..."
)
)
def incorrect_NameError(some_arg):
@@ -294,3 +294,39 @@ def multiple_ifs(some_args):
raise ValueError("...") # this is ok if we don't simplify
else:
pass
def early_return():
if isinstance(this, some_type):
if x in this:
return
raise ValueError(f"{this} has a problem") # this is ok
def early_break():
for x in this:
if isinstance(this, some_type):
if x in this:
break
raise ValueError(f"{this} has a problem") # this is ok
def early_continue():
for x in this:
if isinstance(this, some_type):
if x in this:
continue
raise ValueError(f"{this} has a problem") # this is ok
def early_return_else():
if isinstance(this, some_type):
pass
else:
if x in this:
return
raise ValueError(f"{this} has a problem") # this is ok

View File

@@ -1,27 +1,32 @@
"""
Violation:
Reraise without using 'from'
"""
class MyException(Exception):
pass
class MainFunctionFailed(Exception):
pass
def process():
raise MyException
def bad():
def func():
try:
process()
except MyException:
raise MainFunctionFailed()
a = 1
except Exception:
raise MyException()
def func():
try:
a = 1
except Exception:
if True:
raise MainFunctionFailed()
raise MyException()
def good():
try:
process()
except MyException as ex:
raise MainFunctionFailed() from ex
a = 1
except MyException as e:
raise e # This is verbose violation, shouldn't trigger no cause
except Exception:
raise # Just re-raising don't need 'from'

View File

@@ -16,7 +16,7 @@ resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-s
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
Found 7 errors.
7 potentially fixable with the --fix option.
```
@@ -31,7 +31,7 @@ examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
project/file.py:1:8: F401 `os` imported but unused
project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
Found 7 errors.
7 potentially fixable with the --fix option.
```
@@ -42,7 +42,7 @@ files:
∴ (cd resources/test/project/examples/docs && cargo run .)
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
Found 2 error(s).
Found 2 errors.
2 potentially fixable with the --fix option.
```
@@ -60,7 +60,7 @@ resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
Found 9 error(s).
Found 9 errors.
9 potentially fixable with the --fix option.
```
@@ -73,7 +73,7 @@ docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
Found 4 error(s).
Found 4 errors.
4 potentially fixable with the --fix option.
```
@@ -82,7 +82,7 @@ Passing an excluded directory directly should report errors in the contained fil
```
∴ cargo run resources/test/project/examples/excluded/
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
Found 1 error(s).
Found 1 error.
1 potentially fixable with the --fix option.
```

View File

@@ -1138,10 +1138,6 @@
"PythonVersion": {
"type": "string",
"enum": [
"py33",
"py34",
"py35",
"py36",
"py37",
"py38",
"py39",
@@ -1764,6 +1760,14 @@
"T20",
"T201",
"T203",
"TCH",
"TCH0",
"TCH00",
"TCH001",
"TCH002",
"TCH003",
"TCH004",
"TCH005",
"TID",
"TID2",
"TID25",
@@ -1781,10 +1785,6 @@
"TRY30",
"TRY300",
"TRY301",
"TYP",
"TYP0",
"TYP00",
"TYP005",
"UP",
"UP0",
"UP00",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.233"
version = "0.0.234"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"

View File

@@ -2,6 +2,8 @@
//! to automatically update the `ruff --help` output in the `README.md`.
//!
//! For the actual Ruff library, see [`ruff`].
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate, dead_code)]
mod cli;

View File

@@ -1,10 +1,11 @@
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(
clippy::match_same_arms,
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
use std::io::{self};
use std::path::{Path, PathBuf};
@@ -88,7 +89,7 @@ fn resolve(
}
}
pub fn main() -> Result<ExitCode> {
fn inner_main() -> Result<ExitCode> {
// Extract command-line arguments.
let (cli, overrides) = Cli::parse().partition();
@@ -200,7 +201,7 @@ quoting the executed command, along with the relevant file contents and `pyproje
if cache {
// `--no-cache` doesn't respect code changes, and so is often confusing during
// development.
warn_user_once!("debug build without --no-cache.");
warn_user_once!("Detected debug build without --no-cache.");
}
let printer = Printer::new(&format, &log_level, &autofix, &violations);
@@ -319,3 +320,14 @@ quoting the executed command, along with the relevant file contents and `pyproje
Ok(ExitCode::SUCCESS)
}
#[must_use]
pub fn main() -> ExitCode {
match inner_main() {
Ok(code) => code,
Err(err) => {
eprintln!("{}{} {err:?}", "error".red().bold(), ":".bold());
ExitCode::FAILURE
}
}
}

View File

@@ -96,12 +96,14 @@ impl<'a> Printer<'a> {
let remaining = diagnostics.messages.len();
let total = fixed + remaining;
if fixed > 0 {
let s = if total == 1 { "" } else { "s" };
writeln!(
stdout,
"Found {total} error(s) ({fixed} fixed, {remaining} remaining)."
"Found {total} error{s}) ({fixed} fixed, {remaining} remaining)."
)?;
} else if remaining > 0 {
writeln!(stdout, "Found {remaining} error(s).")?;
let s = if remaining == 1 { "" } else { "s" };
writeln!(stdout, "Found {remaining} error{s}.")?;
}
if !matches!(self.autofix, fix::FixMode::Apply) {
@@ -121,10 +123,11 @@ impl<'a> Printer<'a> {
Violations::Hide => {
let fixed = diagnostics.fixed;
if fixed > 0 {
let s = if fixed == 1 { "" } else { "s" };
if matches!(self.autofix, fix::FixMode::Apply) {
writeln!(stdout, "Fixed {fixed} error(s).")?;
writeln!(stdout, "Fixed {fixed} error{s}.")?;
} else if matches!(self.autofix, fix::FixMode::Diff) {
writeln!(stdout, "Would fix {fixed} error(s).")?;
writeln!(stdout, "Would fix {fixed} error{s}.")?;
}
}
}
@@ -339,8 +342,13 @@ impl<'a> Printer<'a> {
}
if self.log_level >= &LogLevel::Default {
let s = if diagnostics.messages.len() == 1 {
""
} else {
"s"
};
notify_user!(
"Found {} error(s). Watching for file changes.",
"Found {} error{s}. Watching for file changes.",
diagnostics.messages.len()
);
}

View File

@@ -28,7 +28,7 @@ fn test_stdin_error() -> Result<()> {
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"-:1:8: F401 `os` imported but unused\nFound 1 error(s).\n1 potentially fixable with the \
"-:1:8: F401 `os` imported but unused\nFound 1 error.\n1 potentially fixable with the \
--fix option.\n"
);
Ok(())
@@ -44,8 +44,8 @@ fn test_stdin_filename() -> Result<()> {
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"F401.py:1:8: F401 `os` imported but unused\nFound 1 error(s).\n1 potentially fixable \
with the --fix option.\n"
"F401.py:1:8: F401 `os` imported but unused\nFound 1 error.\n1 potentially fixable with \
the --fix option.\n"
);
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.233"
version = "0.0.234"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,8 @@
//! This crate implements an internal CLI for developers of Ruff.
//!
//! Within the ruff repository you can run it with `cargo dev`.
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -13,7 +15,6 @@
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
mod generate_all;
mod generate_cli_help;

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.233"
version = "0.0.234"
edition = "2021"
[lib]

View File

@@ -1,4 +1,6 @@
//! This crate implements internal macros for the `ruff` library.
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -11,7 +13,6 @@
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, ItemFn};

View File

@@ -1,8 +1,7 @@
import os
import re
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ROOT_DIR = Path(__file__).resolve().parent.parent
def dir_name(linter_name: str) -> str:
@@ -15,4 +14,4 @@ def pascal_case(linter_name: str) -> str:
def get_indent(line: str) -> str:
return re.match(r"^\s*", line).group() # pyright: ignore[reportOptionalMemberAccess]
return re.match(r"^\s*", line).group() # type: ignore[union-attr]

View File

@@ -10,15 +10,14 @@ Example usage:
"""
import argparse
import os
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
def main(*, plugin: str, url: str, prefix_code: str) -> None:
"""Generate boilerplate for a new plugin."""
# Create the test fixture folder.
os.makedirs(
ROOT_DIR / "resources/test/fixtures" / dir_name(plugin),
(ROOT_DIR / "resources/test/fixtures" / dir_name(plugin)).mkdir(
exist_ok=True,
)
@@ -56,7 +55,7 @@ mod tests {
}
}
"""
% dir_name(plugin)
% dir_name(plugin),
)
# Create a subdirectory for rules and create a `mod.rs` placeholder
@@ -84,7 +83,7 @@ mod tests {
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == '/// Ruff-specific rules':
elif line.strip() == "/// Ruff-specific rules":
fp.write(f"/// [{plugin}]({url})\n")
fp.write(f'{indent}#[prefix = "{prefix_code}"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")

View File

@@ -16,12 +16,17 @@ from _utils import ROOT_DIR, dir_name, get_indent
def snake_case(name: str) -> str:
"""Convert from PascalCase to snake_case."""
return "".join(f"_{word.lower()}" if word.isupper() else word for word in name).lstrip("_")
return "".join(
f"_{word.lower()}" if word.isupper() else word for word in name
).lstrip("_")
def main(*, name: str, code: str, linter: str) -> None:
"""Generate boilerplate for a new rule."""
# Create a test fixture.
with (ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py").open("a"):
with (ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py").open(
"a",
):
pass
plugin_module = ROOT_DIR / "src/rules" / dir_name(linter)
@@ -35,13 +40,15 @@ def main(*, name: str, code: str, linter: str) -> None:
for line in content.splitlines():
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
indent = get_indent(line)
fp.write(f'{indent}#[test_case(Rule::{name}, Path::new("{code}.py"); "{code}")]')
fp.write(
f'{indent}#[test_case(Rule::{name}, Path::new("{code}.py"); "{code}")]',
)
fp.write("\n")
fp.write(line)
fp.write("\n")
# Add the exports
# Add the exports
rules_dir = plugin_module / "rules"
rules_mod = rules_dir / "mod.rs"
@@ -83,14 +90,14 @@ impl Violation for %s {
}
}
"""
% (name, name)
% (name, name),
)
fp.write("\n")
fp.write(
f"""
/// {code}
pub fn {rule_name_snake}(checker: &mut Checker) {{}}
"""
""",
)
fp.write("\n")

View File

@@ -1,8 +1,10 @@
[tool.ruff]
select = ["ALL"]
ignore = [
"S101", # assert-used
"PLR2004", # magic-value-comparison
"E501", # line-too-long
"INP001", # implicit-namespace-package
"PLR2004", # magic-value-comparison
"S101", # assert-used
]
[tool.ruff.pydocstyle]

View File

@@ -40,19 +40,22 @@ pub fn unparse_stmt(stmt: &Stmt, stylist: &Stylist) -> String {
generator.generate()
}
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) {
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) -> bool {
match &expr.node {
ExprKind::Call { func, .. } => {
collect_call_path_inner(func, parts);
}
ExprKind::Call { func, .. } => collect_call_path_inner(func, parts),
ExprKind::Attribute { value, attr, .. } => {
collect_call_path_inner(value, parts);
parts.push(attr);
if collect_call_path_inner(value, parts) {
parts.push(attr);
true
} else {
false
}
}
ExprKind::Name { id, .. } => {
parts.push(id);
true
}
_ => {}
_ => false,
}
}
@@ -236,6 +239,186 @@ where
}
}
pub fn any_over_stmt<F>(stmt: &Stmt, func: &F) -> bool
where
F: Fn(&Expr) -> bool,
{
match &stmt.node {
StmtKind::FunctionDef {
args,
body,
decorator_list,
returns,
..
}
| StmtKind::AsyncFunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
args.defaults.iter().any(|expr| any_over_expr(expr, func))
|| args
.kw_defaults
.iter()
.any(|expr| any_over_expr(expr, func))
|| args.args.iter().any(|arg| {
arg.node
.annotation
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
})
|| args.kwonlyargs.iter().any(|arg| {
arg.node
.annotation
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
})
|| args.posonlyargs.iter().any(|arg| {
arg.node
.annotation
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
})
|| args.vararg.as_ref().map_or(false, |arg| {
arg.node
.annotation
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
})
|| args.kwarg.as_ref().map_or(false, |arg| {
arg.node
.annotation
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
})
|| body.iter().any(|stmt| any_over_stmt(stmt, func))
|| decorator_list.iter().any(|expr| any_over_expr(expr, func))
|| returns
.as_ref()
.map_or(false, |value| any_over_expr(value, func))
}
StmtKind::ClassDef {
bases,
keywords,
body,
decorator_list,
..
} => {
bases.iter().any(|expr| any_over_expr(expr, func))
|| keywords
.iter()
.any(|keyword| any_over_expr(&keyword.node.value, func))
|| body.iter().any(|stmt| any_over_stmt(stmt, func))
|| decorator_list.iter().any(|expr| any_over_expr(expr, func))
}
StmtKind::Return { value } => value
.as_ref()
.map_or(false, |value| any_over_expr(value, func)),
StmtKind::Delete { targets } => targets.iter().any(|expr| any_over_expr(expr, func)),
StmtKind::Assign { targets, value, .. } => {
targets.iter().any(|expr| any_over_expr(expr, func)) || any_over_expr(value, func)
}
StmtKind::AugAssign { target, value, .. } => {
any_over_expr(target, func) || any_over_expr(value, func)
}
StmtKind::AnnAssign {
target,
annotation,
value,
..
} => {
any_over_expr(target, func)
|| any_over_expr(annotation, func)
|| value
.as_ref()
.map_or(false, |value| any_over_expr(value, func))
}
StmtKind::For {
target,
iter,
body,
orelse,
..
}
| StmtKind::AsyncFor {
target,
iter,
body,
orelse,
..
} => {
any_over_expr(target, func)
|| any_over_expr(iter, func)
|| any_over_body(body, func)
|| any_over_body(orelse, func)
}
StmtKind::While { test, body, orelse } => {
any_over_expr(test, func) || any_over_body(body, func) || any_over_body(orelse, func)
}
StmtKind::If { test, body, orelse } => {
any_over_expr(test, func) || any_over_body(body, func) || any_over_body(orelse, func)
}
StmtKind::With { items, body, .. } | StmtKind::AsyncWith { items, body, .. } => {
items.iter().any(|withitem| {
any_over_expr(&withitem.context_expr, func)
|| withitem
.optional_vars
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
}) || any_over_body(body, func)
}
StmtKind::Raise { exc, cause } => {
exc.as_ref()
.map_or(false, |value| any_over_expr(value, func))
|| cause
.as_ref()
.map_or(false, |value| any_over_expr(value, func))
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
any_over_body(body, func)
|| handlers.iter().any(|handler| {
let ExcepthandlerKind::ExceptHandler { type_, body, .. } = &handler.node;
type_
.as_ref()
.map_or(false, |expr| any_over_expr(expr, func))
|| any_over_body(body, func)
})
|| any_over_body(orelse, func)
|| any_over_body(finalbody, func)
}
StmtKind::Assert { test, msg } => {
any_over_expr(test, func)
|| msg
.as_ref()
.map_or(false, |value| any_over_expr(value, func))
}
// TODO(charlie): Handle match statements.
StmtKind::Match { .. } => false,
StmtKind::Import { .. } => false,
StmtKind::ImportFrom { .. } => false,
StmtKind::Global { .. } => false,
StmtKind::Nonlocal { .. } => false,
StmtKind::Expr { value } => any_over_expr(value, func),
StmtKind::Pass => false,
StmtKind::Break => false,
StmtKind::Continue => false,
}
}
pub fn any_over_body<F>(body: &[Stmt], func: &F) -> bool
where
F: Fn(&Expr) -> bool,
{
body.iter().any(|stmt| any_over_stmt(stmt, func))
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
/// Return `true` if the [`Stmt`] is an assignment to a dunder (like `__all__`).
@@ -379,6 +562,23 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
}
}
/// Return `true` if the body uses `locals()`, `globals()`, `vars()`, `eval()`.
pub fn uses_magic_variable_access(checker: &Checker, body: &[Stmt]) -> bool {
any_over_body(body, &|expr| {
if let ExprKind::Call { func, .. } = &expr.node {
checker.resolve_call_path(func).map_or(false, |call_path| {
call_path.as_slice() == ["", "locals"]
|| call_path.as_slice() == ["", "globals"]
|| call_path.as_slice() == ["", "vars"]
|| call_path.as_slice() == ["", "eval"]
|| call_path.as_slice() == ["", "exec"]
})
} else {
false
}
})
}
/// Format the module name for a relative import.
pub fn format_import_from(level: Option<&usize>, module: Option<&str>) -> String {
let mut module_name = String::with_capacity(16);

View File

@@ -33,6 +33,10 @@ impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range::new(located.location, located.end_location.unwrap())
}
pub fn contains(&self, other: &Range) -> bool {
self.location <= other.location && self.end_location >= other.end_location
}
}
#[derive(Debug)]
@@ -130,8 +134,22 @@ pub struct Binding<'a> {
/// The statement in which the [`Binding`] was defined.
pub source: Option<RefEquality<'a, Stmt>>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used.
pub used: Option<(usize, Range)>,
/// the binding was last used in a runtime context.
pub runtime_usage: Option<(usize, Range)>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used in a typing-time context.
pub typing_usage: Option<(usize, Range)>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used in a synthetic context. This is used for
/// (e.g.) `__future__` imports, explicit re-exports, and other bindings
/// that should be considered used even if they're never referenced.
pub synthetic_usage: Option<(usize, Range)>,
}
#[derive(Copy, Clone)]
pub enum UsageContext {
Runtime,
Typing,
}
// Pyflakes defines the following binding hierarchy (via inheritance):
@@ -152,6 +170,19 @@ pub struct Binding<'a> {
// FutureImportation
impl<'a> Binding<'a> {
pub fn mark_used(&mut self, scope: usize, range: Range, context: UsageContext) {
match context {
UsageContext::Runtime => self.runtime_usage = Some((scope, range)),
UsageContext::Typing => self.typing_usage = Some((scope, range)),
}
}
pub fn used(&self) -> bool {
self.runtime_usage.is_some()
|| self.synthetic_usage.is_some()
|| self.typing_usage.is_some()
}
pub fn is_definition(&self) -> bool {
matches!(
self.kind,

View File

@@ -20,7 +20,7 @@ use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingKind, CallPath, ClassDef, FunctionDef, Lambda, Node, Range, RefEquality, Scope,
ScopeKind,
ScopeKind, UsageContext,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, visitor};
@@ -54,6 +54,7 @@ type DeferralContext<'a> = (Vec<usize>, Vec<RefEquality<'a, Stmt>>);
pub struct Checker<'a> {
// Input data.
path: &'a Path,
package: Option<&'a Path>,
autofix: flags::Autofix,
noqa: flags::Noqa,
pub(crate) settings: &'a Settings,
@@ -92,12 +93,14 @@ pub struct Checker<'a> {
in_deferred_type_definition: bool,
in_literal: bool,
in_subscript: bool,
in_type_checking_block: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) type_checking_blocks: Vec<&'a Stmt>,
}
impl<'a> Checker<'a> {
@@ -108,6 +111,7 @@ impl<'a> Checker<'a> {
autofix: flags::Autofix,
noqa: flags::Noqa,
path: &'a Path,
package: Option<&'a Path>,
locator: &'a Locator,
style: &'a Stylist,
indexer: &'a Indexer,
@@ -118,6 +122,7 @@ impl<'a> Checker<'a> {
autofix,
noqa,
path,
package,
locator,
stylist: style,
indexer,
@@ -149,12 +154,14 @@ impl<'a> Checker<'a> {
in_deferred_type_definition: false,
in_literal: false,
in_subscript: false,
in_type_checking_block: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: path.extension().map_or(false, |ext| ext == "pyi"),
except_handlers: vec![],
// Check-specific state.
flake8_bugbear_seen: vec![],
type_checking_blocks: vec![],
}
}
@@ -329,7 +336,9 @@ where
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Global,
used: usage,
runtime_usage: None,
synthetic_usage: usage,
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
});
@@ -355,7 +364,9 @@ where
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Nonlocal,
used: usage,
runtime_usage: None,
synthetic_usage: usage,
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
});
@@ -369,7 +380,7 @@ where
for index in self.scope_stack.iter().skip(1).rev().skip(1) {
if let Some(index) = self.scopes[*index].values.get(&name.as_str()) {
exists = true;
self.bindings[*index].used = usage;
self.bindings[*index].runtime_usage = usage;
}
}
@@ -667,7 +678,9 @@ where
name,
Binding {
kind: BindingKind::FunctionDefinition,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
},
@@ -819,7 +832,9 @@ where
name,
Binding {
kind: BindingKind::SubmoduleImportation(name, full_name),
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
},
@@ -838,9 +853,10 @@ where
name,
Binding {
kind: BindingKind::Importation(name, full_name),
runtime_usage: None,
// Treat explicit re-export as usage (e.g., `import applications
// as applications`).
used: if alias
synthetic_usage: if alias
.node
.asname
.as_ref()
@@ -857,6 +873,7 @@ where
} else {
None
},
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
},
@@ -1027,7 +1044,9 @@ where
}
}
if self.settings.rules.enabled(&Rule::UnnecessaryFutureImport) {
if self.settings.rules.enabled(&Rule::UnnecessaryFutureImport)
&& self.settings.target_version >= PythonVersion::Py37
{
if let Some("__future__") = module.as_deref() {
pyupgrade::rules::unnecessary_future_import(self, stmt, names);
}
@@ -1086,8 +1105,9 @@ where
name,
Binding {
kind: BindingKind::FutureImportation,
runtime_usage: None,
// Always mark `__future__` imports as used.
used: Some((
synthetic_usage: Some((
self.scopes[*(self
.scope_stack
.last()
@@ -1095,6 +1115,7 @@ where
.id,
Range::from_located(alias),
)),
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
},
@@ -1128,7 +1149,9 @@ where
"*",
Binding {
kind: BindingKind::StarImportation(*level, module.clone()),
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
},
@@ -1182,9 +1205,10 @@ where
name,
Binding {
kind: BindingKind::FromImportation(name, full_name),
runtime_usage: None,
// Treat explicit re-export as usage (e.g., `from .applications
// import FastAPI as FastAPI`).
used: if alias
synthetic_usage: if alias
.node
.asname
.as_ref()
@@ -1201,6 +1225,7 @@ where
} else {
None
},
typing_usage: None,
range,
source: Some(self.current_stmt().clone()),
},
@@ -1377,9 +1402,6 @@ where
if self.settings.rules.enabled(&Rule::IfTuple) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
}
if self.settings.rules.enabled(&Rule::NestedIfStatements) {
flake8_simplify::rules::nested_if_statements(
self,
@@ -1709,7 +1731,9 @@ where
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Assignment,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
});
@@ -1770,7 +1794,9 @@ where
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Assignment,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
});
@@ -1829,6 +1855,25 @@ where
}
self.visit_expr(target);
}
StmtKind::If { test, body, orelse } => {
self.visit_expr(test);
if flake8_type_checking::helpers::is_type_checking_block(self, test) {
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
}
self.type_checking_blocks.push(stmt);
let prev_in_type_checking_block = self.in_type_checking_block;
self.in_type_checking_block = true;
self.visit_body(body);
self.in_type_checking_block = prev_in_type_checking_block;
} else {
self.visit_body(body);
}
self.visit_body(orelse);
}
_ => visitor::walk_stmt(self, stmt),
};
self.visible_scope = prev_visible_scope;
@@ -1844,7 +1889,9 @@ where
name,
Binding {
kind: BindingKind::ClassDefinition,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
},
@@ -3414,7 +3461,7 @@ where
[*(self.scope_stack.last().expect("No current scope found"))];
&scope.values.remove(&name.as_str())
} {
if self.bindings[*index].used.is_none() {
if !self.bindings[*index].used() {
if self.settings.rules.enabled(&Rule::UnusedVariable) {
let mut diagnostic = Diagnostic::new(
violations::UnusedVariable(name.to_string()),
@@ -3532,7 +3579,9 @@ where
&arg.node.arg,
Binding {
kind: BindingKind::Argument,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(arg),
source: Some(self.current_stmt().clone()),
},
@@ -3631,7 +3680,9 @@ impl<'a> Checker<'a> {
self.bindings.push(Binding {
kind: BindingKind::Builtin,
range: Range::default(),
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
source: None,
});
scope.values.insert(builtin, index);
@@ -3716,7 +3767,7 @@ impl<'a> Checker<'a> {
));
}
} else if in_current_scope {
if existing.used.is_none()
if !existing.used()
&& binding.redefines(existing)
&& (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import)
&& !(matches!(existing.kind, BindingKind::FunctionDefinition)
@@ -3749,7 +3800,9 @@ impl<'a> Checker<'a> {
let binding = match scope.values.get(&name) {
None => binding,
Some(index) => Binding {
used: self.bindings[*index].used,
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
..binding
},
};
@@ -3784,8 +3837,18 @@ impl<'a> Checker<'a> {
}
if let Some(index) = scope.values.get(&id.as_str()) {
let context = if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
UsageContext::Typing
} else {
UsageContext::Runtime
};
// Mark the binding as used.
self.bindings[*index].used = Some((scope_id, Range::from_located(expr)));
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
&& !self.in_deferred_string_type_definition
@@ -3813,8 +3876,11 @@ impl<'a> Checker<'a> {
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.values.get(full_name) {
self.bindings[*index].used =
Some((scope_id, Range::from_located(expr)));
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
@@ -3827,8 +3893,11 @@ impl<'a> Checker<'a> {
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.values.get(full_name.as_str()) {
self.bindings[*index].used =
Some((scope_id, Range::from_located(expr)));
self.bindings[*index].mark_used(
scope_id,
Range::from_located(expr),
context,
);
}
}
}
@@ -3956,7 +4025,9 @@ impl<'a> Checker<'a> {
id,
Binding {
kind: BindingKind::Annotation,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
},
@@ -3973,7 +4044,9 @@ impl<'a> Checker<'a> {
id,
Binding {
kind: BindingKind::LoopVar,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
},
@@ -3986,7 +4059,9 @@ impl<'a> Checker<'a> {
id,
Binding {
kind: BindingKind::Binding,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
},
@@ -4036,7 +4111,9 @@ impl<'a> Checker<'a> {
current,
&self.bindings,
)),
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
},
@@ -4049,7 +4126,9 @@ impl<'a> Checker<'a> {
id,
Binding {
kind: BindingKind::Assignment,
used: None,
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
},
@@ -4229,14 +4308,30 @@ impl<'a> Checker<'a> {
}
fn check_dead_scopes(&mut self) {
if !self.settings.rules.enabled(&Rule::UnusedImport)
&& !self.settings.rules.enabled(&Rule::ImportStarUsage)
&& !self.settings.rules.enabled(&Rule::RedefinedWhileUnused)
&& !self.settings.rules.enabled(&Rule::UndefinedExport)
&& !self
if !(self.settings.rules.enabled(&Rule::UnusedImport)
|| self.settings.rules.enabled(&Rule::ImportStarUsage)
|| self.settings.rules.enabled(&Rule::RedefinedWhileUnused)
|| self.settings.rules.enabled(&Rule::UndefinedExport)
|| self
.settings
.rules
.enabled(&Rule::GlobalVariableNotAssigned)
|| self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyFirstPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyThirdPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
return;
}
@@ -4313,7 +4408,7 @@ impl<'a> Checker<'a> {
| BindingKind::FutureImportation
) {
// Skip used exports from `__all__`
if binding.used.is_some()
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
@@ -4369,6 +4464,53 @@ impl<'a> Checker<'a> {
}
}
if self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyFirstPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyThirdPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
for (.., index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
let binding = &self.bindings[*index];
if let Some(diagnostic) =
flake8_type_checking::rules::runtime_import_in_type_checking_block(
binding,
&self.type_checking_blocks,
)
{
diagnostics.push(diagnostic);
}
if let Some(diagnostic) =
flake8_type_checking::rules::typing_only_runtime_import(
binding,
&self.type_checking_blocks,
self.package,
self.settings,
)
{
if self.settings.rules.enabled(diagnostic.kind.rule()) {
diagnostics.push(diagnostic);
}
}
}
}
if self.settings.rules.enabled(&Rule::UnusedImport) {
// Collect all unused imports by location. (Multiple unused imports at the same
// location indicates an `import from`.)
@@ -4395,7 +4537,7 @@ impl<'a> Checker<'a> {
};
// Skip used exports from `__all__`
if binding.used.is_some()
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
@@ -4861,6 +5003,7 @@ pub fn check_ast(
autofix: flags::Autofix,
noqa: flags::Noqa,
path: &Path,
package: Option<&Path>,
) -> Vec<Diagnostic> {
let mut checker = Checker::new(
settings,
@@ -4868,6 +5011,7 @@ pub fn check_ast(
autofix,
noqa,
path,
package,
locator,
stylist,
indexer,

View File

@@ -160,7 +160,7 @@ impl Plugin {
Plugin::Flake8Return => RuleCodePrefix::RET.into(),
Plugin::Flake8Simplify => RuleCodePrefix::SIM.into(),
Plugin::Flake8TidyImports => RuleCodePrefix::TID.into(),
Plugin::Flake8TypeChecking => RuleCodePrefix::TYP.into(),
Plugin::Flake8TypeChecking => RuleCodePrefix::TCH.into(),
Plugin::Flake8UnusedArguments => RuleCodePrefix::ARG.into(),
Plugin::Flake8UsePathlib => RuleCodePrefix::PTH.into(),
Plugin::McCabe => RuleCodePrefix::C9.into(),

View File

@@ -4,6 +4,8 @@
//! and subject to change drastically.
//!
//! [Ruff]: https://github.com/charliermarsh/ruff
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -16,7 +18,6 @@
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
mod ast;
mod autofix;

View File

@@ -98,6 +98,7 @@ pub fn check_path(
autofix,
noqa,
path,
package,
));
}
if use_imports {

View File

@@ -429,7 +429,11 @@ ruff_macros::define_rule_mapping!(
EXE004 => rules::flake8_executable::rules::ShebangWhitespace,
EXE005 => rules::flake8_executable::rules::ShebangNewline,
// flake8-type-checking
TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
TCH001 => rules::flake8_type_checking::rules::TypingOnlyFirstPartyImport,
TCH002 => rules::flake8_type_checking::rules::TypingOnlyThirdPartyImport,
TCH003 => rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport,
TCH004 => rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock,
TCH005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops
TRY004 => rules::tryceratops::rules::PreferTypeError,
TRY200 => rules::tryceratops::rules::ReraiseNoCause,
@@ -580,7 +584,7 @@ pub enum Linter {
#[prefix = "EXE"]
Flake8Executable,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TYP"]
#[prefix = "TCH"]
Flake8TypeChecking,
/// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
#[prefix = "TRY"]

View File

@@ -240,13 +240,16 @@ pub fn python_files_in_path(
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
let mut seen = FxHashSet::default();
if matches!(pyproject_strategy, PyprojectDiscovery::Hierarchical(..)) {
for path in &paths {
for ancestor in path.ancestors() {
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?;
resolver.add(root, settings);
if seen.insert(ancestor) {
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?;
resolver.add(root, settings);
}
}
}
}

View File

@@ -81,5 +81,8 @@ static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
("PDV9", "PD9"),
("PDV90", "PD90"),
("PDV901", "PD901"),
// TODO(charlie): Remove by 2023-04-01.
("TYP", "TCH"),
("TYP001", "TCH001"),
])
});

View File

@@ -22,8 +22,8 @@ use rustc_hash::FxHashMap;
use rustpython_ast::{Expr, ExprKind, Stmt};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::ast::{helpers, visitor};
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
@@ -73,7 +73,7 @@ pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body:
for (name, expr) in control_names {
// Ignore names that are already underscore-prefixed.
if name.starts_with('_') {
if checker.settings.dummy_variable_rgx.is_match(name) {
continue;
}
@@ -82,11 +82,15 @@ pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body:
continue;
}
let safe = !helpers::uses_magic_variable_access(checker, body);
let mut diagnostic = Diagnostic::new(
violations::UnusedLoopControlVariable(name.to_string()),
violations::UnusedLoopControlVariable {
name: name.to_string(),
safe,
},
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
if safe && checker.patch(diagnostic.kind.rule()) {
// Prefix the variable name with an underscore.
diagnostic.amend(Fix::replacement(
format!("_{name}"),

View File

@@ -3,7 +3,9 @@ source: src/rules/flake8_bugbear/mod.rs
expression: diagnostics
---
- kind:
UnusedLoopControlVariable: i
UnusedLoopControlVariable:
name: i
safe: true
location:
row: 6
column: 4
@@ -20,7 +22,9 @@ expression: diagnostics
column: 5
parent: ~
- kind:
UnusedLoopControlVariable: k
UnusedLoopControlVariable:
name: k
safe: true
location:
row: 18
column: 12
@@ -37,7 +41,9 @@ expression: diagnostics
column: 13
parent: ~
- kind:
UnusedLoopControlVariable: i
UnusedLoopControlVariable:
name: i
safe: true
location:
row: 30
column: 4
@@ -54,7 +60,9 @@ expression: diagnostics
column: 5
parent: ~
- kind:
UnusedLoopControlVariable: k
UnusedLoopControlVariable:
name: k
safe: true
location:
row: 30
column: 12
@@ -70,4 +78,52 @@ expression: diagnostics
row: 30
column: 13
parent: ~
- kind:
UnusedLoopControlVariable:
name: bar
safe: false
location:
row: 34
column: 9
end_location:
row: 34
column: 12
fix: ~
parent: ~
- kind:
UnusedLoopControlVariable:
name: bar
safe: false
location:
row: 38
column: 9
end_location:
row: 38
column: 12
fix: ~
parent: ~
- kind:
UnusedLoopControlVariable:
name: bar
safe: false
location:
row: 42
column: 9
end_location:
row: 42
column: 12
fix: ~
parent: ~
- kind:
UnusedLoopControlVariable:
name: bar
safe: false
location:
row: 46
column: 9
end_location:
row: 46
column: 12
fix: ~
parent: ~

View File

@@ -0,0 +1,9 @@
use rustpython_ast::Expr;
use crate::checkers::ast::Checker;
pub fn is_type_checking_block(checker: &Checker, test: &Expr) -> bool {
checker.resolve_call_path(test).map_or(false, |call_path| {
call_path.as_slice() == ["typing", "TYPE_CHECKING"]
})
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-type-checking](https://pypi.org/project/flake8-type-checking/).
pub(crate) mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -13,7 +14,18 @@ mod tests {
use crate::registry::Rule;
use crate::settings;
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TYP005.py"); "TYP005")]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"); "TCH001")]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"); "TCH002")]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"); "TCH003")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_1.py"); "TCH004_1")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_2.py"); "TCH004_2")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_3.py"); "TCH004_3")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_4.py"); "TCH004_4")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_5.py"); "TCH004_5")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_6.py"); "TCH004_6")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"); "TCH004_7")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"); "TCH004_8")]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"); "TCH005")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -17,7 +17,7 @@ impl Violation for EmptyTypeCheckingBlock {
}
}
/// TYP005
/// TCH005
pub fn empty_type_checking_block(checker: &mut Checker, test: &Expr, body: &[Stmt]) {
if checker.resolve_call_path(test).map_or(false, |call_path| {
call_path.as_slice() == ["typing", "TYPE_CHECKING"]

View File

@@ -1,3 +1,12 @@
pub use empty_type_checking_block::{empty_type_checking_block, EmptyTypeCheckingBlock};
pub use runtime_import_in_type_checking_block::{
runtime_import_in_type_checking_block, RuntimeImportInTypeCheckingBlock,
};
pub use typing_only_runtime_import::{
typing_only_runtime_import, TypingOnlyFirstPartyImport, TypingOnlyStandardLibraryImport,
TypingOnlyThirdPartyImport,
};
mod empty_type_checking_block;
mod runtime_import_in_type_checking_block;
mod typing_only_runtime_import;

View File

@@ -0,0 +1,52 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::Stmt;
use crate::ast::types::{Binding, BindingKind, Range};
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct RuntimeImportInTypeCheckingBlock {
pub full_name: String,
}
);
impl Violation for RuntimeImportInTypeCheckingBlock {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Move import `{}` out of type-checking block. Import is used for more than type \
hinting.",
self.full_name
)
}
}
/// TCH004
pub fn runtime_import_in_type_checking_block(
binding: &Binding,
blocks: &[&Stmt],
) -> Option<Diagnostic> {
let full_name = match &binding.kind {
BindingKind::Importation(.., full_name) => full_name,
BindingKind::FromImportation(.., full_name) => full_name.as_str(),
BindingKind::SubmoduleImportation(.., full_name) => full_name,
_ => return None,
};
let defined_in_type_checking = blocks
.iter()
.any(|block| Range::from_located(block).contains(&binding.range));
if defined_in_type_checking {
if binding.runtime_usage.is_some() {
return Some(Diagnostic::new(
RuntimeImportInTypeCheckingBlock {
full_name: full_name.to_string(),
},
binding.range,
));
}
}
None
}

View File

@@ -0,0 +1,126 @@
use std::path::Path;
use ruff_macros::derive_message_formats;
use rustpython_ast::Stmt;
use crate::ast::types::{Binding, BindingKind, Range};
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::rules::isort::{categorize, ImportType};
use crate::settings::Settings;
use crate::violation::Violation;
define_violation!(
pub struct TypingOnlyFirstPartyImport {
pub full_name: String,
}
);
impl Violation for TypingOnlyFirstPartyImport {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Move application import `{}` into a type-checking block",
self.full_name
)
}
}
define_violation!(
pub struct TypingOnlyThirdPartyImport {
pub full_name: String,
}
);
impl Violation for TypingOnlyThirdPartyImport {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Move third-party import `{}` into a type-checking block",
self.full_name
)
}
}
define_violation!(
pub struct TypingOnlyStandardLibraryImport {
pub full_name: String,
}
);
impl Violation for TypingOnlyStandardLibraryImport {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Move standard library import `{}` into a type-checking block",
self.full_name
)
}
}
/// TCH001
pub fn typing_only_runtime_import(
binding: &Binding,
blocks: &[&Stmt],
package: Option<&Path>,
settings: &Settings,
) -> Option<Diagnostic> {
let full_name = match &binding.kind {
BindingKind::Importation(.., full_name) => full_name,
BindingKind::FromImportation(.., full_name) => full_name.as_str(),
BindingKind::SubmoduleImportation(.., full_name) => full_name,
_ => return None,
};
let defined_in_type_checking = blocks
.iter()
.any(|block| Range::from_located(block).contains(&binding.range));
if !defined_in_type_checking {
if binding.typing_usage.is_some()
&& binding.runtime_usage.is_none()
&& binding.synthetic_usage.is_none()
{
// Extract the module base and level from the full name.
// Ex) `foo.bar.baz` -> `foo`, `0`
// Ex) `.foo.bar.baz` -> `foo`, `1`
let module_base = full_name.split('.').next().unwrap();
let level = full_name.chars().take_while(|c| *c == '.').count();
// Categorize the import.
match categorize(
module_base,
Some(&level),
&settings.src,
package,
&settings.isort.known_first_party,
&settings.isort.known_third_party,
&settings.isort.extra_standard_library,
) {
ImportType::LocalFolder | ImportType::FirstParty => {
return Some(Diagnostic::new(
TypingOnlyFirstPartyImport {
full_name: full_name.to_string(),
},
binding.range,
));
}
ImportType::ThirdParty => {
return Some(Diagnostic::new(
TypingOnlyThirdPartyImport {
full_name: full_name.to_string(),
},
binding.range,
));
}
ImportType::StandardLibrary => {
return Some(Diagnostic::new(
TypingOnlyStandardLibraryImport {
full_name: full_name.to_string(),
},
binding.range,
));
}
ImportType::Future => unreachable!("`__future__` imports should be marked as used"),
}
}
}
None
}

View File

@@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: datetime.datetime
location:
row: 4
column: 25
end_location:
row: 4
column: 33
fix: ~
parent: ~

View File

@@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: datetime.date
location:
row: 4
column: 25
end_location:
row: 4
column: 29
fix: ~
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
TypingOnlyFirstPartyImport:
full_name: ".TYP001"
location:
row: 20
column: 18
end_location:
row: 20
column: 24
fix: ~
parent: ~

View File

@@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
TypingOnlyStandardLibraryImport:
full_name: os
location:
row: 8
column: 11
end_location:
row: 8
column: 13
fix: ~
parent: ~

View File

@@ -0,0 +1,93 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas
location:
row: 5
column: 11
end_location:
row: 5
column: 23
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas.DataFrame
location:
row: 11
column: 23
end_location:
row: 11
column: 32
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas.DataFrame
location:
row: 17
column: 23
end_location:
row: 17
column: 38
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas
location:
row: 23
column: 11
end_location:
row: 23
column: 23
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas.DataFrame
location:
row: 29
column: 23
end_location:
row: 29
column: 32
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas.DataFrame
location:
row: 35
column: 23
end_location:
row: 35
column: 38
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas
location:
row: 41
column: 11
end_location:
row: 41
column: 23
fix: ~
parent: ~
- kind:
TypingOnlyThirdPartyImport:
full_name: pandas
location:
row: 47
column: 11
end_location:
row: 47
column: 23
fix: ~
parent: ~

View File

@@ -43,7 +43,7 @@ fn function(
.get(&arg.node.arg.as_str())
.map(|index| &bindings[*index])
{
if binding.used.is_none()
if !binding.used()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{
@@ -88,7 +88,7 @@ fn method(
.get(&arg.node.arg.as_str())
.map(|index| &bindings[*index])
{
if binding.used.is_none()
if !binding.used()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{

View File

@@ -17,6 +17,7 @@ mod tests {
#[test_case(Path::new("import_as.py"); "PTH1_2")]
#[test_case(Path::new("import_from_as.py"); "PTH1_3")]
#[test_case(Path::new("import_from.py"); "PTH1_4")]
#[test_case(Path::new("use_pathlib.py"); "PTH1_5")]
fn rules(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
[]

View File

@@ -3,7 +3,7 @@ use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use categorize::{categorize, ImportType};
pub use categorize::{categorize, ImportType};
use comments::Comment;
use helpers::trailing_comma;
use itertools::Either::{Left, Right};

View File

@@ -1,3 +1,5 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::{Cmpop, Expr, ExprKind};
use crate::ast::helpers::{create_expr, unparse_expr};
@@ -17,3 +19,40 @@ pub fn compare(left: &Expr, ops: &[Cmpop], comparators: &[Expr], stylist: &Styli
stylist,
)
}
static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").unwrap());
pub fn is_overlong(
line: &str,
line_length: usize,
limit: usize,
ignore_overlong_task_comments: bool,
task_tags: &[String],
) -> bool {
if line_length <= limit {
return false;
}
let mut chunks = line.split_whitespace();
let (Some(first), Some(second)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return false;
};
if first == "#" {
if ignore_overlong_task_comments {
let second = second.trim_end_matches(':');
if task_tags.iter().any(|tag| tag == second) {
return false;
}
}
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
if chunks.last().map_or(true, |c| URL_REGEX.is_match(c)) {
return false;
}
}
true
}

View File

@@ -2,7 +2,7 @@ use rustpython_ast::Location;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::rules::is_overlong;
use crate::rules::pycodestyle::helpers::is_overlong;
use crate::settings::Settings;
use crate::violations;

View File

@@ -1,39 +0,0 @@
use once_cell::sync::Lazy;
use regex::Regex;
static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").unwrap());
pub fn is_overlong(
line: &str,
line_length: usize,
limit: usize,
ignore_overlong_task_comments: bool,
task_tags: &[String],
) -> bool {
if line_length <= limit {
return false;
}
let mut chunks = line.split_whitespace();
let (Some(first), Some(second)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return false;
};
if first == "#" {
if ignore_overlong_task_comments {
let second = second.trim_end_matches(':');
if task_tags.iter().any(|tag| tag == second) {
return false;
}
}
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
if chunks.last().map_or(true, |c| URL_REGEX.is_match(c)) {
return false;
}
}
true
}

View File

@@ -2,7 +2,7 @@ use rustpython_ast::Location;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::rules::is_overlong;
use crate::rules::pycodestyle::helpers::is_overlong;
use crate::settings::Settings;
use crate::violations;

View File

@@ -5,7 +5,6 @@ pub use do_not_assign_lambda::do_not_assign_lambda;
pub use do_not_use_bare_except::do_not_use_bare_except;
pub use doc_line_too_long::doc_line_too_long;
pub use invalid_escape_sequence::invalid_escape_sequence;
pub use is_overlong::is_overlong;
pub use line_too_long::line_too_long;
pub use literal_comparisons::literal_comparisons;
pub use mixed_spaces_and_tabs::mixed_spaces_and_tabs;
@@ -20,7 +19,6 @@ mod do_not_assign_lambda;
mod do_not_use_bare_except;
mod doc_line_too_long;
mod invalid_escape_sequence;
mod is_overlong;
mod line_too_long;
mod literal_comparisons;
mod mixed_spaces_and_tabs;

View File

@@ -44,7 +44,7 @@ pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &[Binding]) -> O
for scope in scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(name).map(|index| &bindings[*index]) {
if let Some((scope_id, location)) = binding.used {
if let Some((scope_id, location)) = binding.runtime_usage {
if scope_id == current.id {
return Some(Diagnostic::new(
violations::UndefinedLocal(name.to_string()),

View File

@@ -11,7 +11,7 @@ pub fn unused_annotation(checker: &mut Checker, scope: usize) {
.iter()
.map(|(name, index)| (name, &checker.bindings[*index]))
{
if binding.used.is_none()
if !binding.used()
&& matches!(binding.kind, BindingKind::Annotation)
&& !checker.settings.dummy_variable_rgx.is_match(name)
{

View File

@@ -156,7 +156,7 @@ pub fn unused_variable(checker: &mut Checker, scope: usize) {
.iter()
.map(|(name, index)| (name, &checker.bindings[*index]))
{
if binding.used.is_none()
if !binding.used()
&& matches!(binding.kind, BindingKind::Assignment)
&& !checker.settings.dummy_variable_rgx.is_match(name)
&& name != &"__tracebackhide__"

View File

@@ -6,7 +6,6 @@ use rustpython_parser::ast::Stmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::settings::types::PythonVersion;
use crate::{autofix, violations};
const PY33_PLUS_REMOVE_FUTURES: &[&str] = &[
@@ -34,17 +33,13 @@ const PY37_PLUS_REMOVE_FUTURES: &[&str] = &[
/// UP010
pub fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, names: &[Located<AliasData>]) {
let target_version = checker.settings.target_version;
let mut unused_imports: Vec<&Alias> = vec![];
for alias in names {
if alias.node.asname.is_some() {
continue;
}
if (target_version >= PythonVersion::Py33
&& PY33_PLUS_REMOVE_FUTURES.contains(&alias.node.name.as_str()))
|| (target_version >= PythonVersion::Py37
&& PY37_PLUS_REMOVE_FUTURES.contains(&alias.node.name.as_str()))
if PY33_PLUS_REMOVE_FUTURES.contains(&alias.node.name.as_str())
|| PY37_PLUS_REMOVE_FUTURES.contains(&alias.node.name.as_str())
{
unused_imports.push(alias);
}

View File

@@ -2,6 +2,8 @@ use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::fix::Fix;
@@ -22,6 +24,51 @@ impl AlwaysAutofixableViolation for PreferTypeError {
}
}
#[derive(Default)]
struct ControlFlowVisitor<'a> {
returns: Vec<&'a Stmt>,
breaks: Vec<&'a Stmt>,
continues: Vec<&'a Stmt>,
}
impl<'a, 'b> Visitor<'b> for ControlFlowVisitor<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
match &stmt.node {
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
// Don't recurse.
}
StmtKind::Return { .. } => self.returns.push(stmt),
StmtKind::Break => self.breaks.push(stmt),
StmtKind::Continue => self.continues.push(stmt),
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. }
| ExprKind::GeneratorExp { .. } => {
// Don't recurse.
}
_ => visitor::walk_expr(self, expr),
}
}
}
/// Returns `true` if a [`Stmt`] contains a `return`, `break`, or `continue`.
fn has_control_flow(stmt: &Stmt) -> bool {
let mut visitor = ControlFlowVisitor::default();
visitor.visit_stmt(stmt);
!visitor.returns.is_empty() || !visitor.breaks.is_empty() || !visitor.continues.is_empty()
}
/// Returns `true` if an [`Expr`] is a call to check types.
fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool {
checker.resolve_call_path(call).map_or(false, |call_path| {
@@ -117,8 +164,11 @@ fn check_raise(checker: &mut Checker, exc: &Expr, item: &Stmt) {
}
/// Search the body of an if-condition for raises.
fn check_body(checker: &mut Checker, func: &[Stmt]) {
for item in func {
fn check_body(checker: &mut Checker, body: &[Stmt]) {
for item in body {
if has_control_flow(item) {
return;
}
if let StmtKind::Raise { exc: Some(exc), .. } = &item.node {
check_raise(checker, exc, item);
}
@@ -126,8 +176,11 @@ fn check_body(checker: &mut Checker, func: &[Stmt]) {
}
/// Search the orelse of an if-condition for raises.
fn check_orelse(checker: &mut Checker, func: &[Stmt]) {
for item in func {
fn check_orelse(checker: &mut Checker, body: &[Stmt]) {
for item in body {
if has_control_flow(item) {
return;
}
match &item.node {
StmtKind::If { test, .. } => {
if !check_type_check_test(checker, test) {

View File

@@ -1,5 +1,5 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::{Stmt, StmtKind};
use rustpython_ast::{ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor::{self, Visitor};
@@ -46,8 +46,12 @@ pub fn reraise_no_cause(checker: &mut Checker, body: &[Stmt]) {
};
for stmt in raises {
if let StmtKind::Raise { cause, .. } = &stmt.node {
if cause.is_none() {
if let StmtKind::Raise { exc, cause, .. } = &stmt.node {
if exc
.as_ref()
.map_or(false, |expr| matches!(expr.node, ExprKind::Call { .. }))
&& cause.is_none()
{
checker
.diagnostics
.push(Diagnostic::new(ReraiseNoCause, Range::from_located(stmt)));

View File

@@ -5,21 +5,21 @@ expression: diagnostics
- kind:
ReraiseNoCause: ~
location:
row: 17
row: 15
column: 8
end_location:
row: 17
column: 34
row: 15
column: 27
fix: ~
parent: ~
- kind:
ReraiseNoCause: ~
location:
row: 20
row: 23
column: 12
end_location:
row: 20
column: 38
row: 23
column: 31
fix: ~
parent: ~

View File

@@ -358,7 +358,7 @@ fn resolve_codes<'a>(specs: impl IntoIterator<Item = RuleCodeSpec<'a>>) -> FxHas
for (from, target) in redirects {
// TODO(martin): This belongs into the ruff_cli crate.
crate::warn_user!("`{from}` has been remapped to `{}`", target.as_ref());
crate::warn_user!("`{from}` has been remapped to `{}`.", target.as_ref());
}
rules

View File

@@ -428,6 +428,7 @@ pub struct Options {
#[option_group]
/// Options for the `flake8-bugbear` plugin.
pub flake8_bugbear: Option<flake8_bugbear::settings::Options>,
#[option_group]
/// Options for the `flake8-builtins` plugin.
pub flake8_builtins: Option<flake8_builtins::settings::Options>,
#[option_group]

View File

@@ -11,19 +11,15 @@ use schemars::JsonSchema;
use serde::{de, Deserialize, Deserializer, Serialize};
use super::hashable::HashableHashSet;
use crate::fs;
use crate::registry::Rule;
use crate::rule_selector::RuleSelector;
use crate::{fs, warn_user_once};
#[derive(
Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
@@ -36,16 +32,19 @@ impl FromStr for PythonVersion {
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py33" | "py34" | "py35" | "py36" => {
warn_user_once!(
"Specified a version below the minimum supported Python version. Defaulting \
to Python 3.7."
);
Ok(PythonVersion::Py37)
}
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {string}")),
_ => Err(anyhow!("Unknown version: {string} (try: \"py37\")")),
}
}
}

View File

@@ -1181,18 +1181,35 @@ impl Violation for MutableArgumentDefault {
}
define_violation!(
pub struct UnusedLoopControlVariable(pub String);
pub struct UnusedLoopControlVariable {
/// The name of the loop control variable.
pub name: String,
/// Whether the variable is safe to rename. A variable is unsafe to
/// rename if it _might_ be used by magic control flow (e.g.,
/// `locals()`).
pub safe: bool,
}
);
impl AlwaysAutofixableViolation for UnusedLoopControlVariable {
impl Violation for UnusedLoopControlVariable {
#[derive_message_formats]
fn message(&self) -> String {
let UnusedLoopControlVariable(name) = self;
format!("Loop control variable `{name}` not used within the loop body")
let UnusedLoopControlVariable { name, safe } = self;
if *safe {
format!("Loop control variable `{name}` not used within loop body")
} else {
format!("Loop control variable `{name}` may not be used within loop body")
}
}
fn autofix_title(&self) -> String {
let UnusedLoopControlVariable(name) = self;
format!("Rename unused `{name}` to `_{name}`")
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let UnusedLoopControlVariable { safe, .. } = self;
if *safe {
Some(|UnusedLoopControlVariable { name, .. }| {
format!("Rename unused `{name}` to `_{name}`")
})
} else {
None
}
}
}