Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b07d0bd92 | ||
|
|
23525a8ea0 | ||
|
|
6ede030700 | ||
|
|
44f3e5013d | ||
|
|
35cf9e242e | ||
|
|
d5dff11d4b | ||
|
|
8e1fac620e | ||
|
|
0da691c0d5 | ||
|
|
233415921b | ||
|
|
81141e2a73 | ||
|
|
6e255ad53c | ||
|
|
6d87adbcc0 | ||
|
|
43a8ce6c89 | ||
|
|
a978706dce | ||
|
|
dc1aa8dd1d | ||
|
|
662e29b1ce | ||
|
|
6978dcf035 | ||
|
|
0e6f513607 | ||
|
|
02421d02f5 | ||
|
|
38de46ae3c | ||
|
|
f6fd702d41 | ||
|
|
2125d0bb54 | ||
|
|
63b4f60ba4 | ||
|
|
9eb13bc9da | ||
|
|
0758049e49 | ||
|
|
deff503932 | ||
|
|
82d7814101 | ||
|
|
0cac1a0d21 |
9
.github/workflows/ci.yaml
vendored
9
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
21
.github/workflows/flake8-to-ruff.yaml
vendored
21
.github/workflows/flake8-to-ruff.yaml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/playground.yaml
vendored
1
.github/workflows/playground.yaml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
|
||||
22
.github/workflows/ruff.yaml
vendored
22
.github/workflows/ruff.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
10
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
55
README.md
55
README.md
@@ -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)
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.233"
|
||||
version = "0.0.234"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
27
licenses/LICENSE_snok_flake8-type-checking
Normal file
27
licenses/LICENSE_snok_flake8-type-checking
Normal 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.
|
||||
@@ -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" },
|
||||
|
||||
16
resources/test/fixtures/flake8_bugbear/B007.py
vendored
16
resources/test/fixtures/flake8_bugbear/B007.py
vendored
@@ -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')))
|
||||
|
||||
28
resources/test/fixtures/flake8_type_checking/TCH001.py
vendored
Normal file
28
resources/test/fixtures/flake8_type_checking/TCH001.py
vendored
Normal 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)
|
||||
148
resources/test/fixtures/flake8_type_checking/TCH002.py
vendored
Normal file
148
resources/test/fixtures/flake8_type_checking/TCH002.py
vendored
Normal 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]
|
||||
16
resources/test/fixtures/flake8_type_checking/TCH003.py
vendored
Normal file
16
resources/test/fixtures/flake8_type_checking/TCH003.py
vendored
Normal 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)
|
||||
5
resources/test/fixtures/flake8_type_checking/TCH004_1.py
vendored
Normal file
5
resources/test/fixtures/flake8_type_checking/TCH004_1.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
x = datetime
|
||||
8
resources/test/fixtures/flake8_type_checking/TCH004_2.py
vendored
Normal file
8
resources/test/fixtures/flake8_type_checking/TCH004_2.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import date
|
||||
|
||||
|
||||
def example():
|
||||
return date()
|
||||
6
resources/test/fixtures/flake8_type_checking/TCH004_3.py
vendored
Normal file
6
resources/test/fixtures/flake8_type_checking/TCH004_3.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
CustomType = Any
|
||||
11
resources/test/fixtures/flake8_type_checking/TCH004_4.py
vendored
Normal file
11
resources/test/fixtures/flake8_type_checking/TCH004_4.py
vendored
Normal 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
|
||||
8
resources/test/fixtures/flake8_type_checking/TCH004_5.py
vendored
Normal file
8
resources/test/fixtures/flake8_type_checking/TCH004_5.py
vendored
Normal 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
|
||||
10
resources/test/fixtures/flake8_type_checking/TCH004_6.py
vendored
Normal file
10
resources/test/fixtures/flake8_type_checking/TCH004_6.py
vendored
Normal 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
|
||||
10
resources/test/fixtures/flake8_type_checking/TCH004_7.py
vendored
Normal file
10
resources/test/fixtures/flake8_type_checking/TCH004_7.py
vendored
Normal 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
|
||||
7
resources/test/fixtures/flake8_type_checking/TCH004_8.py
vendored
Normal file
7
resources/test/fixtures/flake8_type_checking/TCH004_8.py
vendored
Normal 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"]()
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass # TYP005
|
||||
pass # TCH005
|
||||
|
||||
|
||||
def example():
|
||||
3
resources/test/fixtures/flake8_use_pathlib/use_pathlib.py
vendored
Normal file
3
resources/test/fixtures/flake8_use_pathlib/use_pathlib.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
(Path("") / "").open()
|
||||
38
resources/test/fixtures/tryceratops/TRY004.py
vendored
38
resources/test/fixtures/tryceratops/TRY004.py
vendored
@@ -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
|
||||
|
||||
37
resources/test/fixtures/tryceratops/TRY200.py
vendored
37
resources/test/fixtures/tryceratops/TRY200.py
vendored
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.233"
|
||||
version = "0.0.234"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.233"
|
||||
version = "0.0.234"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)},")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -98,6 +98,7 @@ pub fn check_path(
|
||||
autofix,
|
||||
noqa,
|
||||
path,
|
||||
package,
|
||||
));
|
||||
}
|
||||
if use_imports {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
9
src/rules/flake8_type_checking/helpers.rs
Normal file
9
src/rules/flake8_type_checking/helpers.rs
Normal 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"]
|
||||
})
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_type_checking/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/rules/flake8_use_pathlib/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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__"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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\")")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user