Compare commits

...

60 Commits

Author SHA1 Message Date
Charlie Marsh
6b4aedb366 Bump version to 0.0.131 2022-11-20 13:40:58 -05:00
Charlie Marsh
8123e3e94e Remove extraneous Python file 2022-11-20 13:39:55 -05:00
Charlie Marsh
9f9a545c51 Improve cache performance by removing cacache dependency (#833) 2022-11-20 13:36:33 -05:00
Charlie Marsh
5bf8219db3 Make main.rs robust to cache initialization failures (#831) 2022-11-20 11:05:17 -05:00
Charlie Marsh
529513bf02 Add CACHEDIR.TAG to .ruff_cache (#830) 2022-11-20 10:53:31 -05:00
Charlie Marsh
124782771f Bump version to 0.0.130 2022-11-20 10:37:19 -05:00
Charlie Marsh
98cab5cdba Add class names to NamedTuple and TypedDict rules (#829) 2022-11-20 10:29:47 -05:00
Martin Lehoux
40f38c94a5 Implement U014: Convert NamedTuple function to class (#819) 2022-11-20 10:26:15 -05:00
Harutaka Kawamura
7839204bf7 Implement autofix for B010 (#823) 2022-11-20 10:14:29 -05:00
Jonathan Plasse
e63ea704f0 Adjust U011 start location (#828) 2022-11-20 10:13:29 -05:00
Charlie Marsh
4be09b45ea Bump version to 0.0.129 2022-11-19 19:52:40 -05:00
Harutaka Kawamura
13e8ed0a0a Implement autofix for E731 (#814) 2022-11-19 19:51:41 -05:00
Anders Kaseorg
4161d4ae32 Exempt parameters with immutable annotations from B006 (#821) 2022-11-19 19:46:08 -05:00
Charlie Marsh
99f7854d8c Mark nonlocal variables as used in parent scopes (#822) 2022-11-19 19:21:02 -05:00
Harutaka Kawamura
a580d1a858 Adjust UnusedNOQA start location (#817) 2022-11-19 09:30:02 -05:00
Martin Lehoux
86806a9e39 U013: Also convert typing.TypedDict (#810) 2022-11-19 09:29:05 -05:00
Charlie Marsh
89afc9db74 Bump version to 0.0.128 2022-11-18 18:50:03 -05:00
Charlie Marsh
0f34cdb7a3 Enable customization of autofixable error codes (#811) 2022-11-18 18:49:13 -05:00
Charlie Marsh
437b6f23b9 Remove warn_on checks (#812) 2022-11-18 18:48:24 -05:00
Charlie Marsh
0fe2b15676 Change NotInTest to NotIsTest 2022-11-18 18:23:40 -05:00
Harutaka Kawamura
e81efa5a3d Implement a --show-source setting (#698) 2022-11-18 14:02:29 -05:00
Charlie Marsh
49559da54e Bump version to 0.0.127 2022-11-18 13:31:22 -05:00
Jonathan Plasse
b74fd1fe13 Change error code of flake8-blind-except (#808) 2022-11-18 13:30:36 -05:00
Charlie Marsh
9c4d24a452 Add flake8-boolean-trap to README 2022-11-18 12:36:13 -05:00
pwoolvett
7a4449eacb Add flake8-boolean-trap (#790) 2022-11-18 12:30:07 -05:00
Charlie Marsh
ee31fa6109 Reduce newlines in code gen (#807) 2022-11-18 12:27:56 -05:00
Harutaka Kawamura
6ffe767252 Implement autofix for E713 and E714 (#804) 2022-11-18 12:16:11 -05:00
Jonathan Plasse
2f894e3951 Add flake8-blind-except (#805) 2022-11-18 12:15:10 -05:00
Charlie Marsh
589d923c99 Misc. follow-ups to #716 (#806) 2022-11-18 12:14:41 -05:00
Martin Lehoux
c5722d8a4d Implement U013: Unnecessary TypedDict syntactic form (#716) 2022-11-18 12:10:47 -05:00
Jonathan Plasse
c2d6307e9b Add missing plugins in some sections of README.md (#802) 2022-11-18 09:28:33 -05:00
Edgar R. M
f44fada446 Implement C901 (mccabe) (#765) 2022-11-17 17:40:50 -05:00
Charlie Marsh
6a6f4651aa Bump version to 0.0.126 2022-11-17 17:19:19 -05:00
Charlie Marsh
66ae4db6cd Ignore globals when checking local variable names (#800) 2022-11-17 17:19:01 -05:00
Charlie Marsh
801c76037f Except BaseException from N818 checks (#798) 2022-11-17 15:04:42 -05:00
Charlie Marsh
ab825eb28d Fix D202 to remove line after docstring (#797) 2022-11-17 15:01:58 -05:00
Charlie Marsh
826ef7da67 Trigger N818 when parent ends in Error or Exception (#796) 2022-11-17 14:51:40 -05:00
Charlie Marsh
72f5393d3a Add flake8-tidy-imports to cache key 2022-11-17 14:46:45 -05:00
Charlie Marsh
6602f7f489 Trim dedented sections for arg detection (#793) 2022-11-17 12:55:55 -05:00
Charlie Marsh
aafddae644 Bump version to 0.0.125 2022-11-17 12:07:05 -05:00
Charlie Marsh
749df87de0 Tweak presentation of null-ls and efm docs (#791) 2022-11-17 12:04:11 -05:00
Eddie Bergman
d67db33f22 docs(integrations): neovim null-ls integration (#782) 2022-11-17 11:55:08 -05:00
Charlie Marsh
f0a54716e5 Implement flake8-tidy-imports (#789) 2022-11-17 11:44:06 -05:00
Harutaka Kawamura
c59e1ff0b5 Implement auto-fix for E711 and E712 (#784) 2022-11-17 11:43:44 -05:00
Jonathan Plasse
ecf858cf16 Add the tools identifier in the TOC (#779) 2022-11-17 11:34:32 -05:00
Jonathan Plasse
8063aee006 Remove unnecessary abspath rule (U002) (#781) 2022-11-17 11:29:42 -05:00
Anders Kaseorg
f1fee5d240 Propagate exit code through Python __main__ wrapper (#776) 2022-11-16 16:16:58 -05:00
Anders Kaseorg
d3155560df Fix find_and_parse_pyproject_toml test for #772 (#774) 2022-11-16 13:47:25 -05:00
Charlie Marsh
90bfc4ec4d Bump version to 0.0.124 2022-11-16 12:25:24 -05:00
Charlie Marsh
b04a6a3f7c Support arbitrary expression paths for class and static decorators (#772) 2022-11-16 12:24:46 -05:00
Charlie Marsh
8ec14e7ee2 Bump version to 0.0.123 2022-11-16 12:06:01 -05:00
Charlie Marsh
17c5cd7c42 Fix off-by-one in noqa map detection (#771)
Fix off-by-one in noqa
2022-11-16 12:00:10 -05:00
Charlie Marsh
7d8360a1de Change all &Option<> to Option<&> (#768) 2022-11-16 09:40:01 -05:00
Harutaka Kawamura
910ee523dd Fix E731 (#766) 2022-11-16 09:17:14 -05:00
Charlie Marsh
72e35a535e Run cargo fmt 2022-11-16 09:15:22 -05:00
Charlie Marsh
b4e1563517 Avoid allocations for binding values (#764) 2022-11-16 08:55:35 -05:00
Charlie Marsh
5717cc97d7 Add references to Flake8 licenses 2022-11-15 23:07:41 -05:00
Charlie Marsh
2c89a19f76 Bump Ruff version to 0.0.122 2022-11-15 22:03:46 -05:00
Charlie Marsh
82fea36bb3 Preserve comments when sorting imports (#749) 2022-11-15 22:02:52 -05:00
Charlie Marsh
63d63e8c12 Increase retry counts in GitHub Actions workflows (#763) 2022-11-15 17:21:16 -05:00
159 changed files with 5954 additions and 2110 deletions

View File

@@ -6,6 +6,11 @@ on:
pull_request:
branches: [main]
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
cargo_build:
name: "cargo build"

View File

@@ -10,6 +10,9 @@ env:
PACKAGE_NAME: flake8-to-ruff
CRATE_NAME: flake8_to_ruff
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
macos-x86_64:

View File

@@ -12,6 +12,9 @@ concurrency:
env:
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
macos-x86_64:

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.121
rev: v0.0.131
hooks:
- id: ruff

View File

@@ -98,6 +98,13 @@ _and_ a `pyproject.toml` parameter to `src/pyproject.rs`. If you want to pattern
existing example, grep for `dummy_variable_rgx`, which defines a regular expression to match against
acceptable unused variables (e.g., `_`).
If the new plugin's configuration should be cached between runs, you'll need to add it to the
`Hash` implementation for `Settings` in `src/settings/mod.rs`.
You may also want to add the new configuration option to the `flake8-to-ruff` tool, which is
responsible for converting `flake8` configuration files to Ruff's TOML format. This logic
lives in `flake8_to_ruff/src/converter.rs`.
## Release process
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub

655
Cargo.lock generated
View File

@@ -49,6 +49,16 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7"
[[package]]
name = "annotate-snippets"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36"
dependencies = [
"unicode-width",
"yansi-term",
]
[[package]]
name = "anyhow"
version = "1.0.66"
@@ -84,133 +94,6 @@ dependencies = [
"wait-timeout",
]
[[package]]
name = "async-channel"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-executor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"once_cell",
"slab",
]
[[package]]
name = "async-global-executor"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
dependencies = [
"async-channel",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7"
dependencies = [
"async-lock",
"autocfg",
"concurrent-queue",
"futures-lite",
"libc",
"log",
"parking",
"polling",
"slab",
"socket2",
"waker-fn",
"winapi 0.3.9",
]
[[package]]
name = "async-lock"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
dependencies = [
"event-listener",
"futures-lite",
]
[[package]]
name = "async-process"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c"
dependencies = [
"async-io",
"autocfg",
"blocking",
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
"libc",
"once_cell",
"signal-hook",
"winapi 0.3.9",
]
[[package]]
name = "async-std"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
"async-channel",
"async-global-executor",
"async-io",
"async-lock",
"async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-task"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
[[package]]
name = "atomic-waker"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
[[package]]
name = "atty"
version = "0.2.14"
@@ -228,15 +111,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]]
name = "base64"
version = "0.13.1"
@@ -273,50 +147,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
dependencies = [
"block-padding",
"byte-tools",
"byteorder",
"generic-array 0.12.4",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.6",
]
[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
dependencies = [
"byte-tools",
]
[[package]]
name = "blocking"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
dependencies = [
"async-channel",
"async-task",
"atomic-waker",
"fastrand",
"futures-lite",
"once_cell",
]
[[package]]
name = "bstr"
version = "0.2.17"
@@ -347,46 +177,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byte-tools"
version = "0.3.1"
name = "cachedir"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cacache"
version = "10.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c13caedf5b624de6448b78e980320a27266557350198dc67cf64bc8561c27e8"
checksum = "e236bf5873ea57ec2877445297f4da008916bfae51567131acfc54a073d694f3"
dependencies = [
"async-std",
"digest 0.9.0",
"either",
"futures",
"hex 0.4.3",
"memmap2",
"serde",
"serde_derive",
"serde_json",
"sha-1 0.9.8",
"sha2 0.9.9",
"ssri",
"tempfile",
"thiserror",
"walkdir",
]
[[package]]
name = "cache-padded"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "cast"
version = "0.3.0"
@@ -417,7 +215,7 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c"
dependencies = [
"annotate-snippets",
"annotate-snippets 0.6.1",
]
[[package]]
@@ -572,15 +370,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101"
[[package]]
name = "concurrent-queue"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
dependencies = [
"cache-padded",
]
[[package]]
name = "configparser"
version = "3.0.2"
@@ -606,15 +395,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -709,16 +489,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "cxx"
version = "1.0.81"
@@ -775,24 +545,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.6",
]
[[package]]
name = "directories"
version = "4.0.1"
@@ -880,18 +632,6 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fastrand"
version = "1.8.0"
@@ -930,7 +670,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.121-dev.0"
version = "0.0.131-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -1004,129 +744,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "futures"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
[[package]]
name = "futures-task"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]]
name = "futures-util"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -1157,18 +774,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "gloo-timers"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "half"
version = "1.8.2"
@@ -1199,18 +804,6 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexf-parse"
version = "0.2.1"
@@ -1353,15 +946,6 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]]
name = "lalrpop"
version = "0.19.8"
@@ -1498,7 +1082,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
"value-bag",
]
[[package]]
@@ -1522,15 +1105,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memmap2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.6.5"
@@ -1717,30 +1291,12 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "os_str_bytes"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "parking_lot"
version = "0.12.1"
@@ -1913,18 +1469,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "plotters"
version = "0.3.4"
@@ -1953,20 +1497,6 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"libc",
"log",
"wepoll-ffi",
"winapi 0.3.9",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -2238,14 +1768,15 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.121"
version = "0.0.131"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
"assert_cmd",
"atty",
"bincode",
"bitflags",
"cacache",
"cachedir",
"chrono",
"clap 4.0.22",
"clearscreen",
@@ -2287,7 +1818,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.121"
version = "0.0.131"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -2468,75 +1999,6 @@ dependencies = [
"serde",
]
[[package]]
name = "sha-1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug 0.2.3",
]
[[package]]
name = "sha-1"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [
"block-buffer 0.9.0",
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]
name = "sha2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
dependencies = [
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug 0.2.3",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]
name = "signal-hook"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "similar"
version = "2.2.0"
@@ -2570,37 +2032,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "ssri"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9cec0d388f39fbe79d7aa600e8d38053bf97b1bc8d350da7c0ba800d0f423f2"
dependencies = [
"base64 0.10.1",
"digest 0.8.1",
"hex 0.3.2",
"serde",
"sha-1 0.8.2",
"sha2 0.8.2",
"thiserror",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -2845,12 +2282,6 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -2989,7 +2420,7 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
dependencies = [
"base64 0.13.1",
"base64",
"chunked_transfer",
"flate2",
"log",
@@ -3013,16 +2444,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
dependencies = [
"ctor",
"version_check",
]
[[package]]
name = "version_check"
version = "0.9.4"
@@ -3044,12 +2465,6 @@ dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.2"
@@ -3098,18 +2513,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
@@ -3168,15 +2571,6 @@ dependencies = [
"webpki",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
dependencies = [
"cc",
]
[[package]]
name = "which"
version = "4.3.0"
@@ -3312,3 +2706,12 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi-term"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
dependencies = [
"winapi 0.3.9",
]

View File

@@ -6,17 +6,19 @@ members = [
[package]
name = "ruff"
version = "0.0.121"
version = "0.0.131"
edition = "2021"
[lib]
name = "ruff"
[dependencies]
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { version = "1.0.66" }
atty = { version = "0.2.14" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
cachedir = { version = "0.3.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
clap = { version = "4.0.1", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -51,7 +53,6 @@ update-informer = { version = "0.5.0", default-features = false, features = ["py
walkdir = { version = "2.3.2" }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
cacache = { version = "10.0.1" } # uses async-std
clearscreen = { version = "1.0.10" } # uses which
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support

424
LICENSE
View File

@@ -19,3 +19,427 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The externally maintained libraries from which parts of the Software is derived
are:
- Pyflakes, licensed as follows:
"""
Copyright 2005-2011 Divmod, Inc.
Copyright 2013-2014 Florent Xicluna
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
- autoflake, licensed as follows:
"""
Copyright (C) 2012-2018 Steven Myint
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- Flake8, licensed as follows:
"""
== Flake8 License (MIT) ==
Copyright (C) 2011-2013 Tarek Ziade <tarek@ziade.org>
Copyright (C) 2012-2016 Ian Cordasco <graffatcolmingov@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-2020, licensed as follows:
"""
Copyright (c) 2019 Anthony Sottile
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-annotations, licensed as follows:
"""
MIT License
Copyright (c) 2019 - Present S. Co1
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-bandit, licensed as follows:
"""
Copyright (c) 2017 Tyler Wince
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-blind-except, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2014 Elijah Andrews
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
- flake8-bugbear, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2016 Łukasz Langa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-comprehensions, licensed as follows:
"""
MIT License
Copyright (c) 2017 Adam Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-tidy-imports, licensed as follows:
"""
MIT License
Copyright (c) 2017 Adam Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-print, licensed as follows:
"""
MIT License
Copyright (c) 2016 Joseph Kahn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-quotes, licensed as follows:
"""
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- isort, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2013 Timothy Edmund Crosley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- pep8-naming, licensed as follows:
"""
Copyright © 2013 Florent Xicluna <florent.xicluna@gmail.com>
Licensed under the terms of the Expat License
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pycodestyle, licensed as follows:
"""
Copyright © 2006-2009 Johann C. Rocholl <johann@rocholl.net>
Copyright © 2009-2014 Florent Xicluna <florent.xicluna@gmail.com>
Copyright © 2014-2020 Ian Lee <IanLee1521@gmail.com>
Licensed under the terms of the Expat License
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pydocstyle, licensed as follows:
"""
Copyright (c) 2012 GreenSteam, <http://greensteam.dk/>
Copyright (c) 2014-2020 Amir Rachum, <http://amir.rachum.com/>
Copyright (c) 2020 Sambhav Kothari, <https://github.com/samj1912>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pyupgrade, licensed as follows:
"""
Copyright (c) 2017 Anthony Sottile
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

204
README.md
View File

@@ -44,31 +44,35 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
## Table of Contents
1. [Installation and Usage](#installation-and-usage)
2. [Configuration](#configuration)
3. [Supported Rules](#supported-rules)
1. [Pyflakes](#pyflakes)
2. [pycodestyle](#pycodestyle)
3. [isort](#isort)
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-bandit](#flake8-bandit)
8. [flake8-comprehensions](#flake8-comprehensions)
9. [flake8-bugbear](#flake8-bugbear)
10. [flake8-builtins](#flake8-builtins)
11. [flake8-print](#flake8-print)
12. [flake8-quotes](#flake8-quotes)
13. [flake8-annotations](#flake8-annotations)
14. [flake8-2020](#flake8-2020)
15. [Ruff-specific rules](#ruff-specific-rules)
16. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
8. [Releases](#releases)
9. [Benchmarks](#benchmarks)
10. [License](#license)
11. [Contributing](#contributing)
1. [Configuration](#configuration)
1. [Supported Rules](#supported-rules)
1. [Pyflakes (F)](#pyflakes)
1. [pycodestyle (E)](#pycodestyle)
1. [isort (I)](#isort)
1. [pydocstyle (D)](#pydocstyle)
1. [pyupgrade (U)](#pyupgrade)
1. [pep8-naming (N)](#pep8-naming)
1. [flake8-bandit (S)](#flake8-bandit)
1. [flake8-comprehensions (C)](#flake8-comprehensions)
1. [flake8-bugbear (B)](#flake8-bugbear)
1. [flake8-builtins (A)](#flake8-builtins)
1. [flake8-tidy-imports (I25)](#flake8-tidy-imports)
1. [flake8-print (T)](#flake8-print)
1. [flake8-quotes (Q)](#flake8-quotes)
1. [flake8-annotations (ANN)](#flake8-annotations)
1. [flake8-2020 (YTT)](#flake8-2020)
1. [flake8-blind-except (BLE)](#flake8-blind-except)
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap)
1. [mccabe (C90)](#mccabe)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules)
1. [Meta rules (M)](#meta-rules)
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
1. [Development](#development)
1. [Releases](#releases)
1. [Benchmarks](#benchmarks)
1. [License](#license)
1. [Contributing](#contributing)
## Installation and Usage
@@ -118,7 +122,7 @@ default configuration is equivalent to:
[tool.ruff]
line-length = 88
# Enable Flake's "E" and "F" codes by default.
# Enable Pyflakes `E` and `F` codes by default.
select = ["E", "F"]
ignore = []
@@ -153,18 +157,24 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py310"
```
As an example, the following would configure Ruff to (1) avoid checking for line-length
violations (`E501`) and (2) ignore unused import rules in `__init__.py` files:
As an example, the following would configure Ruff to: (1) avoid checking for line-length
violations (`E501`); (2), always autofix, but never remove unused imports (`F401`); and (3) ignore
import-at-top-of-file errors (`E402`) in `__init__.py` files:
```toml
[tool.ruff]
# Enable Pyflakes and pycodestyle rules.
select = ["E", "F"]
# Never enforce `E501`.
# Never enforce `E501` (line length violations).
ignore = ["E501"]
# Ignore `F401` violations in any `__init__.py` file, and in `path/to/file.py`.
per-file-ignores = {"__init__.py" = ["F401"], "path/to/file.py" = ["F401"]}
# Always autofix, but never try to fix `F401` (unused imports).
fix = true
unfixable = ["F401"]
# Ignore `E402` (import violations in any `__init__.py` file, and in `path/to/file.py`.
per-file-ignores = {"__init__.py" = ["E402"], "path/to/file.py" = ["E402"]}
```
Plugin configurations should be expressed as subsections, e.g.:
@@ -187,7 +197,7 @@ ruff path/to/code/ --select F401 --select F403
See `ruff --help` for more:
```shell
ruff: An extremely fast Python linter.
Ruff: An extremely fast Python linter.
Usage: ruff [OPTIONS] <FILES>...
@@ -223,14 +233,20 @@ Options:
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>
Like --exclude, but adds additional files and directories on top of the excluded ones
--fixable <FIXABLE>
List of error codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--unfixable <UNFIXABLE>
List of error codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text, json]
--show-source
Show violations with source code
--show-files
See the files ruff will be run against with the current settings
See the files Ruff will be run against with the current settings
--show-settings
See ruff's settings
See Ruff's settings
--add-noqa
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
@@ -239,6 +255,8 @@ Options:
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated checks and automatic formatting
--max-complexity <MAX_COMPLEXITY>
Max McCabe complexity allowed for a function
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
@@ -356,13 +374,13 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| ---- | ---- | ------- | --- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | |
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | |
| E713 | NotInTest | Test for membership should be `not in` | |
| E714 | NotIsTest | Test for object identity should be `is not` | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | 🛠 |
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | 🛠 |
| E713 | NotInTest | Test for membership should be `not in` | 🛠 |
| E714 | NotIsTest | Test for object identity should be `is not` | 🛠 |
| E721 | TypeComparison | Do not compare types, use `isinstance()` | |
| E722 | DoNotUseBareExcept | Do not use bare `except` | |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | 🛠 |
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | |
| E742 | AmbiguousClassName | Ambiguous class name: `...` | |
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
@@ -437,7 +455,6 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 |
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 |
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 |
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 |
@@ -448,6 +465,8 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| U010 | UnnecessaryFutureImport | Unnecessary `__future__` import `...` for target Python version | 🛠 |
| U011 | UnnecessaryLRUCacheParams | Unnecessary parameters to `functools.lru_cache` | 🛠 |
| U012 | UnnecessaryEncodeUTF8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
| U013 | ConvertTypedDictFunctionalToClass | Convert `TypedDict` functional syntax to class syntax | 🛠 |
| U014 | ConvertNamedTupleFunctionalToClass | Convert `NamedTuple` functional syntax to class syntax | 🛠 |
### pep8-naming
@@ -507,6 +526,16 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 |
| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | |
### flake8-boolean-trap
For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/0.1.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| FBT001 | BooleanPositionalArgInFunctionDefinition | Boolean positional arg in function definition | |
| FBT002 | BooleanDefaultValueInFunctionDefinition | Boolean default value in function definition | |
| FBT003 | BooleanPositionalValueInFunctionCall | Boolean positional value in function call | |
### flake8-bugbear
For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/) on PyPI.
@@ -521,7 +550,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function call in argument defaults | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B012 | JumpStatementInFinally | `return/continue/break` inside finally blocks cause exceptions to be silenced | |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError` instead of `except (ValueError,)`. | |
@@ -549,6 +578,14 @@ For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/2.0.1/)
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | |
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | |
### flake8-tidy-imports
For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/4.8.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I252 | BannedRelativeImport | Relative imports are banned | |
### flake8-print
For more, see [flake8-print](https://pypi.org/project/flake8-print/5.0.0/) on PyPI.
@@ -604,6 +641,22 @@ For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI
| YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | |
| YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | |
### flake8-blind-except
For more, see [flake8-blind-except](https://pypi.org/project/flake8-blind-except/0.2.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| BLE001 | BlindExcept | Blind except Exception: statement | |
### mccabe
For more, see [mccabe](https://pypi.org/project/mccabe/0.7.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C901 | FunctionIsTooComplex | `...` is too complex (10) | |
### Ruff-specific rules
| Code | Name | Message | Fix |
@@ -643,8 +696,59 @@ Ruff should then appear as a runnable action:
Ruff is available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright) extension
for coc.nvim.
Ruff can also be integrated via [efm](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#efm)
in just a [few lines](https://github.com/JafarAbdi/myconfigs/blob/6f0b6b2450e92ec8fc50422928cd22005b919110/efm-langserver/config.yaml#L14-L20).
<details>
<summary>Ruff can also be integrated via <a href="https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#efm"><code>efm</code></a> in just a <a href="https://github.com/JafarAbdi/myconfigs/blob/6f0b6b2450e92ec8fc50422928cd22005b919110/efm-langserver/config.yaml#L14-L20">few lines</a>.</summary>
<br>
```yaml
tools:
python-ruff: &python-ruff
lint-command: 'ruff --config ~/myconfigs/linters/ruff.toml --quiet ${INPUT}'
lint-stdin: true
lint-formats:
- '%f:%l:%c: %m'
format-command: 'ruff --stdin-filename ${INPUT} --config ~/myconfigs/linters/ruff.toml --fix --exit-zero --quiet -'
format-stdin: true
```
</details>
<details>
<summary>For neovim users using <a href="https://github.com/jose-elias-alvarez/null-ls.nvim"><code>null-ls</code></a>, Ruff is already <a href="https://github.com/jose-elias-alvarez/null-ls.nvim">integrated</a>.</summary>
<br>
```lua
local null_ls = require("null-ls")
local methods = require("null-ls.methods")
local helpers = require("null-ls.helpers")
local function ruff_fix()
return helpers.make_builtin({
name = "ruff",
meta = {
url = "https://github.com/charliermarsh/ruff/",
description = "An extremely fast Python linter, written in Rust.",
},
method = methods.internal.FORMATTING,
filetypes = { "python" },
generator_opts = {
command = "ruff",
args = { "--fix", "-e", "-n", "--stdin-filename", "$FILENAME", "-" },
to_stdin = true
},
factory = helpers.formatter_factory
})
end
null_ls.setup({
sources = {
ruff_fix(),
null_ls.builtins.diagnostics.ruff,
}
})
```
</details>
### Language Server Protocol (Unofficial)
@@ -705,6 +809,7 @@ including:
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
@@ -712,7 +817,11 @@ including:
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`isort`](https://pypi.org/project/isort/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/33)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
@@ -726,10 +835,12 @@ Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis F
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
@@ -737,9 +848,12 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
- [`mccabe`](https://pypi.org/project/mccabe/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34).
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/33).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.121-dev.0"
version = "0.0.131-dev.0"
edition = "2021"
[lib]

View File

@@ -3,9 +3,12 @@ use std::collections::{BTreeSet, HashMap};
use anyhow::Result;
use ruff::checks_gen::CheckCodePrefix;
use ruff::flake8_quotes::settings::Quote;
use ruff::flake8_tidy_imports::settings::Strictness;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{flake8_annotations, flake8_bugbear, flake8_quotes, pep8_naming};
use ruff::{
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming,
};
use crate::plugin::Plugin;
use crate::{parser, plugin};
@@ -71,6 +74,8 @@ pub fn convert(
let mut flake8_annotations: flake8_annotations::settings::Options = Default::default();
let mut flake8_bugbear: flake8_bugbear::settings::Options = Default::default();
let mut flake8_quotes: flake8_quotes::settings::Options = Default::default();
let mut flake8_tidy_imports: flake8_tidy_imports::settings::Options = Default::default();
let mut mccabe: mccabe::settings::Options = Default::default();
let mut pep8_naming: pep8_naming::settings::Options = Default::default();
for (key, value) in flake8 {
if let Some(value) = value {
@@ -172,10 +177,23 @@ pub fn convert(
pep8_naming.staticmethod_decorators =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-tidy-imports
"ban-relative-imports" | "ban_relative_imports" => match value.trim() {
"true" => flake8_tidy_imports.ban_relative_imports = Some(Strictness::All),
"parents" => {
flake8_tidy_imports.ban_relative_imports = Some(Strictness::Parents)
}
_ => eprintln!("Unexpected '{key}' value: {value}"),
},
// flake8-docstrings
"docstring-convention" => {
// No-op (handled above).
}
// mccabe
"max-complexity" | "max_complexity" => match value.clone().parse::<usize>() {
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
},
// Unknown
_ => eprintln!("Skipping unsupported property: {key}"),
}
@@ -194,6 +212,12 @@ pub fn convert(
if flake8_quotes != Default::default() {
options.flake8_quotes = Some(flake8_quotes);
}
if flake8_tidy_imports != Default::default() {
options.flake8_tidy_imports = Some(flake8_tidy_imports);
}
if mccabe != Default::default() {
options.mccabe = Some(mccabe);
}
if pep8_naming != Default::default() {
options.pep8_naming = Some(pep8_naming);
}
@@ -219,26 +243,31 @@ mod tests {
fn it_converts_empty() -> Result<()> {
let actual = convert(&HashMap::from([]), None)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: None,
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -253,26 +282,31 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: Some(100),
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -287,26 +321,31 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: Some(100),
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -321,26 +360,31 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: None,
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -355,22 +399,25 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: None,
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
@@ -379,7 +426,9 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -397,11 +446,16 @@ mod tests {
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: None,
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::D100,
CheckCodePrefix::D101,
@@ -442,16 +496,16 @@ mod tests {
CheckCodePrefix::F,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -466,23 +520,26 @@ mod tests {
None,
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
fix: None,
fixable: None,
ignore: Some(vec![]),
line_length: None,
per_file_ignores: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::Q,
CheckCodePrefix::W,
]),
extend_select: None,
ignore: Some(vec![]),
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
show_source: None,
src: None,
target_version: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
@@ -491,7 +548,9 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
flake8_tidy_imports: None,
isort: None,
mccabe: None,
pep8_naming: None,
});
assert_eq!(actual, expected);

View File

@@ -11,9 +11,12 @@ pub enum Plugin {
Flake8Builtins,
Flake8Comprehensions,
Flake8Docstrings,
Flake8TidyImports,
Flake8Print,
Flake8Quotes,
Flake8Annotations,
McCabe,
Flake8BlindExcept,
PEP8Naming,
Pyupgrade,
}
@@ -28,9 +31,12 @@ impl FromStr for Plugin {
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-annotations" => Ok(Plugin::Flake8Annotations),
"flake8-blind-except" => Ok(Plugin::Flake8BlindExcept),
"mccabe" => Ok(Plugin::McCabe),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pyupgrade" => Ok(Plugin::Pyupgrade),
_ => Err(anyhow!("Unknown plugin: {}", string)),
@@ -44,11 +50,14 @@ impl Plugin {
Plugin::Flake8Bandit => CheckCodePrefix::S,
Plugin::Flake8Bugbear => CheckCodePrefix::B,
Plugin::Flake8Builtins => CheckCodePrefix::A,
Plugin::Flake8Comprehensions => CheckCodePrefix::C,
Plugin::Flake8Comprehensions => CheckCodePrefix::C4,
Plugin::Flake8Docstrings => CheckCodePrefix::D,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::Flake8Print => CheckCodePrefix::T,
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Annotations => CheckCodePrefix::ANN,
Plugin::Flake8BlindExcept => CheckCodePrefix::BLE,
Plugin::McCabe => CheckCodePrefix::C9,
Plugin::PEP8Naming => CheckCodePrefix::N,
Plugin::Pyupgrade => CheckCodePrefix::U,
}
@@ -59,7 +68,7 @@ impl Plugin {
Plugin::Flake8Bandit => vec![CheckCodePrefix::S],
Plugin::Flake8Bugbear => vec![CheckCodePrefix::B],
Plugin::Flake8Builtins => vec![CheckCodePrefix::A],
Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C],
Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C4],
Plugin::Flake8Docstrings => {
// Use the user-provided docstring.
for key in ["docstring-convention", "docstring_convention"] {
@@ -76,9 +85,12 @@ impl Plugin {
// Default to PEP8.
DocstringConvention::PEP8.select()
}
Plugin::Flake8TidyImports => vec![CheckCodePrefix::I25],
Plugin::Flake8Print => vec![CheckCodePrefix::T],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Annotations => vec![CheckCodePrefix::ANN],
Plugin::Flake8BlindExcept => vec![CheckCodePrefix::BLE],
Plugin::McCabe => vec![CheckCodePrefix::C9],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
Plugin::Pyupgrade => vec![CheckCodePrefix::U],
}
@@ -315,6 +327,17 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
"allow-star-arg-any" | "allow_star_arg_any" => {
plugins.insert(Plugin::Flake8Annotations);
}
// flake8-tidy-imports
"ban-relative-imports" | "ban_relative_imports" => {
plugins.insert(Plugin::Flake8TidyImports);
}
"banned-modules" | "banned_modules" => {
plugins.insert(Plugin::Flake8TidyImports);
}
// mccabe
"max-complexity" | "max_complexity" => {
plugins.insert(Plugin::McCabe);
}
// pep8-naming
"ignore-names" | "ignore_names" => {
plugins.insert(Plugin::PEP8Naming);
@@ -342,9 +365,11 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Builtins,
Plugin::Flake8Comprehensions,
Plugin::Flake8Docstrings,
Plugin::Flake8TidyImports,
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Annotations,
Plugin::Flake8BlindExcept,
Plugin::PEP8Naming,
Plugin::Pyupgrade,
]

View File

@@ -185,3 +185,23 @@ def nested_b008(a=random.randint(0, dt.datetime.now().year)):
# Ignore lambda contents since they are evaluated at call time.
def foo(f=lambda x: print(x)):
f(1)
from collections import abc
from typing import Annotated, Dict, Optional, Sequence, Union, Set
def immutable_annotations(
a: Sequence[int] | None = [],
b: Optional[abc.Mapping[int, int]] = {},
c: Annotated[Union[abc.Set[str], abc.Sized], "annotation"] = set(),
):
pass
def mutable_annotations(
a: list[int] | None = [],
b: Optional[Dict[int, int]] = {},
c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
):
pass

View File

@@ -34,3 +34,4 @@ setattr(foo, "bar", None)
setattr(foo, "_123abc", None)
setattr(foo, "abc123", None)
setattr(foo, r"abc123", None)
setattr(foo.bar, r"baz", None)

55
resources/test/fixtures/BLE.py vendored Normal file
View File

@@ -0,0 +1,55 @@
try:
pass
except ValueError:
pass
except Exception as e:
raise e
finally:
pass
try:
pass
except BaseException as e:
raise e
except TypeError:
pass
else:
pass
try:
pass
except Exception as e:
raise e
except BaseException:
pass
try:
pass
except Exception:
pass
finally:
try:
pass
except BaseException as e:
raise e
try:
pass
except Exception as e:
try:
raise e
except BaseException:
pass
try:
try:
pass
except BaseException as e:
raise e
except Exception:
pass

108
resources/test/fixtures/C901.py vendored Normal file
View File

@@ -0,0 +1,108 @@
# Complexity = 1
def trivial():
pass
# Complexity = 1
def expr_as_statement():
0xF00D
# Complexity = 1
def sequential(n):
k = n + 4
s = k + n
return s
# Complexity = 3
def if_elif_else_dead_path(n):
if n > 3:
return "bigger than three"
elif n > 4:
return "is never executed"
else:
return "smaller than or equal to three"
# Complexity = 3
def nested_ifs():
if n > 3:
if n > 4:
return "bigger than four"
else:
return "bigger than three"
else:
return "smaller than or equal to three"
# Complexity = 2
def for_loop():
for i in range(10):
print(i)
# Complexity = 2
def for_else(mylist):
for i in mylist:
print(i)
else:
print(None)
# Complexity = 2
def recursive(n):
if n > 4:
return f(n - 1)
else:
return n
# Complexity = 3
def nested_functions():
def a():
def b():
pass
b()
a()
# Complexity = 4
def try_else():
try:
print(1)
except TypeA:
print(2)
except TypeB:
print(3)
else:
print(4)
# Complexity = 3
def nested_try_finally():
try:
try:
print(1)
finally:
print(2)
finally:
print(3)
# Complexity = 3
async def foobar(a, b, c):
await whatever(a, b, c)
if await b:
pass
async with c:
pass
async for x in a:
pass
# Complexity = 1
def annotated_assign():
x: Any = None

View File

@@ -532,3 +532,37 @@ class Blah: # noqa: D203,D213
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
'D100: Missing docstring in public module')
@expect('D201: No blank lines allowed before function docstring (found 1)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multiline_leading_space():
"""Leading space.
More content.
"""
@expect('D202: No blank lines allowed after function docstring (found 1)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multiline_trailing_space():
"""Leading space.
More content.
"""
pass
@expect('D201: No blank lines allowed before function docstring (found 1)')
@expect('D202: No blank lines allowed after function docstring (found 1)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multiline_trailing_and_leading_space():
"""Trailing and leading space.
More content.
"""
pass

View File

@@ -5,7 +5,7 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = """Lorem ipsum dolor sit amet.
_ = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533

View File

@@ -23,6 +23,9 @@ if None != res[1]:
if None == res[1]:
pass
if x == None != None:
pass
#: Okay
if x not in y:
pass

View File

@@ -22,6 +22,9 @@ var = 1 if cond == True else -1 if cond == False else cond
if (True) == TrueElement or x == TrueElement:
pass
if res == True != False:
pass
#: Okay
if x not in y:
pass

View File

@@ -8,14 +8,14 @@ while False:
f = object()
#: E731
f.method = lambda: "Method"
f = {}
#: E731
f["a"] = lambda x: x ** 2
f = []
f.append(lambda x: x ** 2)
f = g = lambda x: x ** 2
lambda: "no-op"

View File

@@ -35,3 +35,20 @@ def f4():
_ = 1
__ = 1
_discarded = 1
a = 1
def f5():
global a
# Used in `f7` via `nonlocal`.
b = 1
def f6():
# F841
b = 1
def f7():
nonlocal b

42
resources/test/fixtures/FBT.py vendored Normal file
View File

@@ -0,0 +1,42 @@
def function(
posonly_nohint,
posonly_nonboolhint: int,
posonly_boolhint: bool,
posonly_boolstrhint: "bool",
/,
offset,
posorkw_nonvalued_nohint,
posorkw_nonvalued_nonboolhint: int,
posorkw_nonvalued_boolhint: bool,
posorkw_nonvalued_boolstrhint: "bool",
posorkw_boolvalued_nohint=True,
posorkw_boolvalued_nonboolhint: int = True,
posorkw_boolvalued_boolhint: bool = True,
posorkw_boolvalued_boolstrhint: "bool" = True,
posorkw_nonboolvalued_nohint=1,
posorkw_nonboolvalued_nonboolhint: int = 2,
posorkw_nonboolvalued_boolhint: bool = 3,
posorkw_nonboolvalued_boolstrhint: "bool" = 4,
*,
kwonly_nonvalued_nohint,
kwonly_nonvalued_nonboolhint: int,
kwonly_nonvalued_boolhint: bool,
kwonly_nonvalued_boolstrhint: "bool",
kwonly_boolvalued_nohint=True,
kwonly_boolvalued_nonboolhint: int = False,
kwonly_boolvalued_boolhint: bool = True,
kwonly_boolvalued_boolstrhint: "bool" = True,
kwonly_nonboolvalued_nohint=5,
kwonly_nonboolvalued_nonboolhint: int = 1,
kwonly_nonboolvalued_boolhint: bool = 1,
kwonly_nonboolvalued_boolstrhint: "bool" = 1,
**kw,
):
...
def used(do):
return do
used("a", True)
used(do=True)

12
resources/test/fixtures/I252.py vendored Normal file
View File

@@ -0,0 +1,12 @@
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
import other
import other.example
from other import example

View File

@@ -18,6 +18,14 @@ def f() -> None:
# Invalid (and unimplemented)
d = 1 # noqa: F841, W191
# fmt: off
# Invalid - no space before #
d = 1# noqa: E501
# Invalid - many spaces before #
d = 1 # noqa: E501
# fmt: on
# Valid
_ = """Lorem ipsum dolor sit amet.

View File

@@ -1,5 +1,7 @@
from abc import ABCMeta
import pydantic
class Class:
def bad_method(this):
@@ -21,6 +23,14 @@ class Class:
def static_method(x):
return x
@pydantic.validator
def lower(cls, my_field: str) -> str:
pass
@pydantic.validator("my_field")
def lower(cls, my_field: str) -> str:
pass
def __init__(self):
...

View File

@@ -1,8 +1,12 @@
import collections
from collections import namedtuple
GLOBAL: str = "foo"
def f():
global GLOBAL
GLOBAL = "bar"
lower = 0
Camel = 0
CONSTANT = 0

View File

@@ -8,3 +8,11 @@ class AnotherError(Exception):
class C(Exception):
pass
class D(BaseException):
pass
class E(AnotherError):
pass

View File

@@ -1,15 +0,0 @@
from os.path import abspath
x = abspath(__file__)
import os
y = os.path.abspath(__file__)
from os import path
z = path.abspath(__file__)

33
resources/test/fixtures/U013.py vendored Normal file
View File

@@ -0,0 +1,33 @@
from typing import TypedDict, NotRequired, Literal
import typing
# dict literal
MyType1 = TypedDict("MyType1", {"a": int, "b": str})
# dict call
MyType2 = TypedDict("MyType2", dict(a=int, b=str))
# kwargs
MyType3 = TypedDict("MyType3", a=int, b=str)
# Empty TypedDict
MyType4 = TypedDict("MyType4")
# Literal values
MyType5 = TypedDict("MyType5", {"a": "hello"})
MyType6 = TypedDict("MyType6", a="hello")
# NotRequired
MyType7 = TypedDict("MyType7", {"a": NotRequired[dict]})
# total
MyType8 = TypedDict("MyType8", {"x": int, "y": int}, total=False)
# invalid identifiers
MyType9 = TypedDict("MyType9", {"in": int, "x-y": int})
# using Literal type
MyType10 = TypedDict("MyType10", {"key": Literal["value"]})
# using namespace TypedDict
MyType11 = typing.TypedDict("MyType11", {"key": int})

22
resources/test/fixtures/U014.py vendored Normal file
View File

@@ -0,0 +1,22 @@
from typing import NamedTuple
import typing
# with complex annotations
NT1 = NamedTuple("NT1", [("a", int), ("b", tuple[str, ...])])
# with default values as list
NT2 = NamedTuple(
"NT2",
[("a", int), ("b", str), ("c", list[bool])],
defaults=["foo", [True]],
)
# with namespace
NT3 = typing.NamedTuple("NT3", [("a", int), ("b", str)])
# with too many default values
NT4 = NamedTuple(
"NT4",
[("a", int), ("b", str)],
defaults=[1, "bar", "baz"],
)

View File

@@ -0,0 +1,7 @@
import os
# This is a comment in the same section, so we need to add one newline.
import sys
import numpy as np
# This is a comment, but it starts a new section, so we don't need to add a newline
# before it.
import leading_prefix

View File

@@ -0,0 +1,25 @@
# Comment 1
# Comment 2
import D
# Comment 3a
import C
# Comment 3b
import C
import B # Comment 4
# Comment 5
# Comment 6
from A import (
a, # Comment 7
b,
c, # Comment 8
)
from A import (
a, # Comment 9
b, # Comment 10
c, # Comment 11
)

View File

@@ -0,0 +1,4 @@
import a
# Don't take this comment into account when determining whether the next import can fit on one line.
from b import c
from d import e # Do take this comment into account when determining whether the next import can fit on one line.

View File

@@ -0,0 +1,11 @@
import io
# Old MacDonald had a farm,
# EIEIO
# And on his farm he had a cow,
# EIEIO
# With a moo-moo here and a moo-moo there
# Here a moo, there a moo, everywhere moo-moo
# Old MacDonald had a farm,
# EIEIO
from errno import EIO
import abc

View File

@@ -0,0 +1,2 @@
import A # type: ignore
from B import C # type: ignore

View File

@@ -16,6 +16,9 @@ multiline-quotes = "double"
docstring-quotes = "double"
avoid-escape = true
[tool.ruff.mccabe]
max-complexity = 10
[tool.ruff.pep8-naming]
ignore-names = [
"setUp",
@@ -33,7 +36,11 @@ ignore-names = [
]
classmethod-decorators = [
"classmethod",
"pydantic.validator",
]
staticmethod-decorators = [
"staticmethod",
]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"

View File

@@ -4,4 +4,4 @@ import sysconfig
if __name__ == "__main__":
ruff = os.path.join(sysconfig.get_path("scripts"), "ruff")
os.spawnv(os.P_WAIT, ruff, [ruff, *sys.argv[1:]])
sys.exit(os.spawnv(os.P_WAIT, ruff, [ruff, *sys.argv[1:]]))

View File

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

View File

@@ -1,7 +1,10 @@
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::SourceCodeLocator;
#[inline(always)]
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
@@ -225,6 +228,29 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
}
}
/// Format the module name for a relative import.
pub fn format_import_from(level: Option<&usize>, module: Option<&String>) -> String {
let mut module_name = String::with_capacity(16);
if let Some(level) = level {
for _ in 0..*level {
module_name.push('.');
}
}
if let Some(module) = module {
module_name.push_str(module);
}
module_name
}
/// Split a target string (like `typing.List`) into (`typing`, `List`).
pub fn to_module_and_member(target: &str) -> (&str, &str) {
if let Some(index) = target.rfind('.') {
(&target[..index], &target[index + 1..])
} else {
("", target)
}
}
/// Convert a location within a file (relative to `base`) to an absolute
/// position.
pub fn to_absolute(relative: &Location, base: &Location) -> Location {
@@ -238,6 +264,34 @@ pub fn to_absolute(relative: &Location, base: &Location) -> Location {
}
}
/// Return `true` if a `Stmt` has leading content.
pub fn match_leading_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
let range = Range {
location: Location::new(stmt.location.row(), 0),
end_location: stmt.location,
};
let prefix = locator.slice_source_code_range(&range);
prefix.chars().any(|char| !char.is_whitespace())
}
/// Return `true` if a `Stmt` has trailing content.
pub fn match_trailing_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
let range = Range {
location: stmt.end_location.unwrap(),
end_location: Location::new(stmt.end_location.unwrap().row() + 1, 0),
};
let suffix = locator.slice_source_code_range(&range);
for char in suffix.chars() {
if char == '#' {
return false;
}
if !char.is_whitespace() {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -3,3 +3,4 @@ pub mod operations;
pub mod relocate;
pub mod types;
pub mod visitor;
pub mod whitespace;

View File

@@ -19,9 +19,7 @@ impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range {
location: located.location,
end_location: located
.end_location
.expect("AST nodes should have end_location."),
end_location: located.end_location.unwrap(),
}
}
}
@@ -54,7 +52,7 @@ pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: FnvHashMap<String, Binding>,
pub values: FnvHashMap<&'a str, Binding>,
}
impl<'a> Scope<'a> {
@@ -79,14 +77,17 @@ pub enum BindingKind {
Annotation,
Argument,
Assignment,
// TODO(charlie): This seems to be a catch-all.
Binding,
LoopVar,
Global,
Nonlocal,
Builtin,
ClassDefinition,
Definition,
Export(Vec<String>),
FutureImportation,
StarImportation,
StarImportation(Option<usize>, Option<String>),
Importation(String, String, BindingContext),
FromImportation(String, String, BindingContext),
SubmoduleImportation(String, String, BindingContext),

View File

@@ -3,12 +3,6 @@ use rustpython_ast::{Located, Location};
use crate::ast::types::Range;
use crate::check_ast::Checker;
pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
"ur\"\"\"", "ur'''", "u\"\"\"", "u'''", "r\"\"\"", "r'''", "\"\"\"", "'''",
];
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &["ur\"", "ur'", "u\"", "u'", "r\"", "r'", "\"", "'"];
/// Extract the leading words from a line of text.
pub fn leading_words(line: &str) -> String {
line.trim()

View File

@@ -50,4 +50,15 @@ impl Fix {
applied: false,
}
}
pub fn dummy(location: Location) -> Self {
Self {
patch: Patch {
content: "".to_string(),
location,
end_location: location,
},
applied: false,
}
}
}

View File

@@ -1,19 +1,11 @@
// cacache uses asyncd-std which has no wasm support, so currently no caching
// support on wasm
#![cfg_attr(
target_family = "wasm",
allow(unused_imports, unused_variables, dead_code)
)]
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
use anyhow::Result;
#[cfg(not(target_family = "wasm"))]
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use path_absolutize::Absolutize;
@@ -82,28 +74,59 @@ fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String {
fn content_dir() -> &'static str {
"content"
}
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> u64 {
let mut hasher = DefaultHasher::new();
CARGO_PKG_VERSION.hash(&mut hasher);
path.absolutize().unwrap().hash(&mut hasher);
settings.hash(&mut hasher);
autofix.hash(&mut hasher);
format!(
"{}@{}@{}",
path.absolutize().unwrap().to_string_lossy(),
CARGO_PKG_VERSION,
hasher.finish()
hasher.finish()
}
/// Initialize the cache directory.
pub fn init() -> Result<()> {
let path = Path::new(cache_dir());
// Create the cache directories.
create_dir_all(path.join(content_dir()))?;
// Add the CACHEDIR.TAG.
if !cachedir::is_tagged(path)? {
cachedir::add_tag(path)?;
}
// Add the .gitignore.
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
let mut file = File::create(gitignore_path)?;
file.write_all(b"*")?;
}
Ok(())
}
fn write_sync(key: &u64, value: &[u8]) -> Result<(), std::io::Error> {
fs::write(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
value,
)
}
pub fn init() -> Result<()> {
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
if gitignore_path.exists() {
return Ok(());
}
create_dir_all(cache_dir())?;
let mut file = File::create(gitignore_path)?;
file.write_all(b"*").map_err(|e| e.into())
fn read_sync(key: &u64) -> Result<Vec<u8>, std::io::Error> {
fs::read(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
)
}
/// Get a value from the cache.
pub fn get(
path: &Path,
metadata: &Metadata,
@@ -115,9 +138,8 @@ pub fn get(
return None;
};
#[cfg(not(target_family = "wasm"))] // cacache needs async-std which doesn't support wasm
match cacache::read_sync(cache_dir(), cache_key(path, settings, autofix)) {
Ok(encoded) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
if let Ok(encoded) = read_sync(&cache_key(path, settings, autofix)) {
match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
@@ -127,13 +149,12 @@ pub fn get(
}
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
},
Err(EntryNotFound(..)) => {}
Err(e) => error!("Failed to read from cache: {e:?}"),
}
}
None
}
/// Set a value in the cache.
pub fn set(
path: &Path,
metadata: &Metadata,
@@ -146,18 +167,15 @@ pub fn set(
return;
};
#[cfg(not(target_family = "wasm"))] // modification date not supported on wasm
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
},
messages,
};
#[cfg(not(target_family = "wasm"))] // cacache needs async-std which doesn't support wasm
if let Err(e) = cacache::write_sync(
cache_dir(),
cache_key(path, settings, autofix),
bincode::serialize(&check_result).unwrap(),
if let Err(e) = write_sync(
&cache_key(path, settings, autofix),
&bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}")
}

View File

@@ -19,8 +19,7 @@ use crate::ast::helpers::{
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Range, Scope,
ScopeKind,
Binding, BindingContext, BindingKind, ClassScope, ImportKind, Range, Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{helpers, operations, visitor};
@@ -36,8 +35,9 @@ use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins,
flake8_comprehensions, flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print,
flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -152,10 +152,10 @@ impl<'a> Checker<'a> {
/// Return `true` if a patch should be generated under the given autofix
/// `Mode`.
pub fn patch(&self) -> bool {
pub fn patch(&self, code: &CheckCode) -> bool {
// TODO(charlie): We can't fix errors in f-strings until RustPython adds
// location data.
self.autofix.patch() && self.in_f_string.is_none()
self.autofix.patch() && self.in_f_string.is_none() && self.settings.fixable.contains(code)
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
@@ -213,22 +213,72 @@ where
// Pre-visit.
match &stmt.node {
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
let global_scope_id = self.scopes[GLOBAL_SCOPE_INDEX].id;
let current_scope = self.current_scope();
let current_scope_id = current_scope.id;
if current_scope_id != global_scope_id {
StmtKind::Global { names } => {
let scope_index = *self.scope_stack.last().expect("No current scope found.");
if scope_index != GLOBAL_SCOPE_INDEX {
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for name in names {
for scope in self.scopes.iter_mut().skip(GLOBAL_SCOPE_INDEX + 1) {
scope.values.insert(
name.to_string(),
Binding {
kind: BindingKind::Assignment,
used: Some((global_scope_id, Range::from_located(stmt))),
range: Range::from_located(stmt),
},
);
// Add a binding to the current scope.
scope.values.insert(
name,
Binding {
kind: BindingKind::Global,
used: usage,
range: Range::from_located(stmt),
},
);
}
// Mark the binding in the global scope as used.
for name in names {
if let Some(mut existing) = self.scopes[GLOBAL_SCOPE_INDEX]
.values
.get_mut(&name.as_str())
{
existing.used = usage;
}
}
}
if self.settings.enabled.contains(&CheckCode::E741) {
let location = Range::from_located(stmt);
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, location)
})
.into_iter(),
);
}
}
StmtKind::Nonlocal { names } => {
let scope_index = *self.scope_stack.last().expect("No current scope found.");
if scope_index != GLOBAL_SCOPE_INDEX {
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for name in names {
// Add a binding to the current scope.
scope.values.insert(
name,
Binding {
kind: BindingKind::Global,
used: usage,
range: Range::from_located(stmt),
},
);
}
// Mark the binding in the defining scopes as used too. (Skip the global scope
// and the current scope.)
for name in names {
for index in self.scope_stack.iter().skip(1).rev().skip(1) {
if let Some(mut existing) =
self.scopes[*index].values.get_mut(&name.as_str())
{
existing.used = usage;
}
}
}
}
@@ -309,6 +359,8 @@ where
name,
decorator_list,
args,
&self.from_imports,
&self.import_aliases,
&self.settings.pep8_naming,
)
{
@@ -322,6 +374,8 @@ where
name,
decorator_list,
args,
&self.from_imports,
&self.import_aliases,
&self.settings.pep8_naming,
) {
self.add_check(check);
@@ -348,6 +402,16 @@ where
if self.settings.enabled.contains(&CheckCode::B019) {
flake8_bugbear::plugins::cached_instance_method(self, decorator_list);
}
if self.settings.enabled.contains(&CheckCode::C901) {
if let Some(check) = mccabe::checks::function_is_too_complex(
stmt,
name,
body,
self.settings.mccabe.max_complexity,
) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::S107) {
self.add_checks(
@@ -397,7 +461,7 @@ where
self.visit_expr(expr);
}
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::Definition,
used: None,
@@ -502,7 +566,7 @@ where
let name = alias.node.name.split('.').next().unwrap();
let full_name = &alias.node.name;
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::SubmoduleImportation(
name.to_string(),
@@ -524,7 +588,7 @@ where
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
let full_name = &alias.node.name;
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::Importation(
name.to_string(),
@@ -657,7 +721,7 @@ where
if let Some("__future__") = module.as_deref() {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::FutureImportation,
// Always mark `__future__` imports as used.
@@ -694,16 +758,10 @@ where
));
}
} else if alias.node.name == "*" {
let module_name = format!(
"{}{}",
".".repeat(level.unwrap_or_default()),
module.clone().unwrap_or_else(|| "module".to_string()),
);
self.add_binding(
module_name.to_string(),
"*",
Binding {
kind: BindingKind::StarImportation,
kind: BindingKind::StarImportation(*level, module.clone()),
used: None,
range: Range::from_located(stmt),
},
@@ -714,7 +772,10 @@ where
[*(self.scope_stack.last().expect("No current scope found."))];
if !matches!(scope.kind, ScopeKind::Module) {
self.add_check(Check::new(
CheckKind::ImportStarNotPermitted(module_name.to_string()),
CheckKind::ImportStarNotPermitted(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
)),
Range::from_located(stmt),
));
}
@@ -722,15 +783,16 @@ where
if self.settings.enabled.contains(&CheckCode::F403) {
self.add_check(Check::new(
CheckKind::ImportStarUsed(module_name.to_string()),
CheckKind::ImportStarUsed(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
)),
Range::from_located(stmt),
));
}
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
scope.import_starred = true;
} else {
if let Some(asname) = &alias.node.asname {
@@ -746,7 +808,7 @@ where
Some(parent) => format!("{}.{}", parent, alias.node.name),
};
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::FromImportation(
name.to_string(),
@@ -778,6 +840,16 @@ where
)
}
if self.settings.enabled.contains(&CheckCode::I252) {
if let Some(check) = flake8_tidy_imports::checks::banned_relative_import(
stmt,
level.as_ref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
) {
self.add_check(check);
}
}
if let Some(asname) = &alias.node.asname {
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
@@ -862,7 +934,12 @@ where
pyflakes::plugins::assert_tuple(self, stmt, test);
}
if self.settings.enabled.contains(&CheckCode::B011) {
flake8_bugbear::plugins::assert_false(self, stmt, test, msg);
flake8_bugbear::plugins::assert_false(
self,
stmt,
test,
msg.as_ref().map(|expr| expr.deref()),
);
}
if self.settings.enabled.contains(&CheckCode::S101) {
self.add_check(flake8_bandit::plugins::assert_used(stmt));
@@ -897,13 +974,14 @@ where
if self.settings.enabled.contains(&CheckCode::B013) {
flake8_bugbear::plugins::redundant_tuple_in_exception_handler(self, handlers);
}
if self.settings.enabled.contains(&CheckCode::BLE001) {
flake8_blind_except::plugins::blind_except(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(check) =
pycodestyle::checks::do_not_assign_lambda(value, Range::from_located(stmt))
{
self.add_check(check);
if let [target] = &targets[..] {
pycodestyle::plugins::do_not_assign_lambda(self, target, value, stmt)
}
}
if self.settings.enabled.contains(&CheckCode::U001) {
@@ -919,16 +997,21 @@ where
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::U013) {
pyupgrade::plugins::convert_typed_dict_functional_to_class(
self, stmt, targets, value,
);
}
if self.settings.enabled.contains(&CheckCode::U014) {
pyupgrade::plugins::convert_named_tuple_functional_to_class(
self, stmt, targets, value,
);
}
}
StmtKind::AnnAssign { value, .. } => {
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(value) = value {
if let Some(check) = pycodestyle::checks::do_not_assign_lambda(
value,
Range::from_located(stmt),
) {
self.add_check(check);
}
pycodestyle::plugins::do_not_assign_lambda(self, target, value, stmt);
}
}
}
@@ -1017,7 +1100,7 @@ where
if let StmtKind::ClassDef { name, .. } = &stmt.node {
self.pop_scope();
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::ClassDefinition,
used: None,
@@ -1130,7 +1213,7 @@ where
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.handle_node_store(expr, self.current_parent());
self.handle_node_store(id, expr, self.current_parent());
}
ExprContext::Del => self.handle_node_delete(expr),
}
@@ -1223,7 +1306,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C400),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1237,7 +1320,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C401),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1251,7 +1334,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C402),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1266,7 +1349,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C403),
Range::from_located(expr),
)
{
@@ -1282,7 +1365,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C404),
Range::from_located(expr),
)
{
@@ -1297,7 +1380,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C405),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1311,7 +1394,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C406),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1325,7 +1408,7 @@ where
args,
keywords,
self.locator,
self.patch(),
self.patch(&CheckCode::C408),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1339,7 +1422,7 @@ where
func,
args,
self.locator,
self.patch(),
self.patch(&CheckCode::C409),
Range::from_located(expr),
)
{
@@ -1354,7 +1437,7 @@ where
func,
args,
self.locator,
self.patch(),
self.patch(&CheckCode::C410),
Range::from_located(expr),
)
{
@@ -1368,7 +1451,7 @@ where
func,
args,
self.locator,
self.patch(),
self.patch(&CheckCode::C411),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1382,7 +1465,7 @@ where
func,
args,
self.locator,
self.patch(),
self.patch(&CheckCode::C413),
Range::from_located(expr),
)
{
@@ -1425,27 +1508,22 @@ where
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::U002)
&& self.settings.target_version >= PythonVersion::Py310
{
pyupgrade::plugins::unnecessary_abspath(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::U003) {
pyupgrade::plugins::type_of_primitive(self, expr, func, args);
}
// flake8-boolean-trap
if self.settings.enabled.contains(&CheckCode::FBT003) {
flake8_boolean_trap::plugins::check_boolean_positional_value_in_function_call(
self, args,
);
}
if let ExprKind::Name { id, ctx } = &func.node {
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: false })
) {
scope.kind = ScopeKind::Function(FunctionScope { uses_locals: true });
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if let ScopeKind::Function(inner) = &mut scope.kind {
inner.uses_locals = true;
}
}
}
@@ -1503,9 +1581,13 @@ where
let check_not_in = self.settings.enabled.contains(&CheckCode::E713);
let check_not_is = self.settings.enabled.contains(&CheckCode::E714);
if check_not_in || check_not_is {
self.add_checks(
pycodestyle::checks::not_tests(op, operand, check_not_in, check_not_is)
.into_iter(),
pycodestyle::plugins::not_tests(
self,
expr,
op,
operand,
check_not_in,
check_not_is,
);
}
@@ -1521,16 +1603,15 @@ where
let check_none_comparisons = self.settings.enabled.contains(&CheckCode::E711);
let check_true_false_comparisons = self.settings.enabled.contains(&CheckCode::E712);
if check_none_comparisons || check_true_false_comparisons {
self.add_checks(
pycodestyle::checks::literal_comparisons(
left,
ops,
comparators,
check_none_comparisons,
check_true_false_comparisons,
)
.into_iter(),
);
pycodestyle::plugins::literal_comparisons(
self,
expr,
left,
ops,
comparators,
check_none_comparisons,
check_true_false_comparisons,
)
}
if self.settings.enabled.contains(&CheckCode::F632) {
@@ -1636,7 +1717,7 @@ where
elt,
generators,
self.locator,
self.patch(),
self.patch(&CheckCode::C416),
Range::from_located(expr),
) {
self.add_check(check);
@@ -1860,8 +1941,9 @@ where
false,
);
if self.current_scope().values.contains_key(name) {
if self.current_scope().values.contains_key(&name.as_str()) {
self.handle_node_store(
name,
&Expr::new(
excepthandler.location,
excepthandler.end_location.unwrap(),
@@ -1874,8 +1956,9 @@ where
);
}
let definition = self.current_scope().values.get(name).cloned();
let definition = self.current_scope().values.get(&name.as_str()).cloned();
self.handle_node_store(
name,
&Expr::new(
excepthandler.location,
excepthandler.end_location.unwrap(),
@@ -1892,7 +1975,7 @@ where
if let Some(binding) = {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
&scope.values.remove(name)
&scope.values.remove(&name.as_str())
} {
if binding.used.is_none() {
if self.settings.enabled.contains(&CheckCode::F841) {
@@ -1907,7 +1990,7 @@ where
if let Some(binding) = definition {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
scope.values.insert(name.to_string(), binding);
scope.values.insert(name, binding);
}
}
None => walk_excepthandler(self, excepthandler),
@@ -1928,6 +2011,16 @@ where
flake8_bugbear::plugins::function_call_argument_default(self, arguments)
}
// flake8-boolean-trap
if self.settings.enabled.contains(&CheckCode::FBT001) {
flake8_boolean_trap::plugins::check_positional_boolean_in_def(self, arguments);
}
if self.settings.enabled.contains(&CheckCode::FBT002) {
flake8_boolean_trap::plugins::check_boolean_default_value_in_function_definition(
self, arguments,
);
}
// Bind, but intentionally avoid walking default expressions, as we handle them
// upstream.
for arg in &arguments.posonlyargs {
@@ -1951,7 +2044,7 @@ where
// Bind, but intentionally avoid walking the annotation, as we handle it
// upstream.
self.add_binding(
arg.node.arg.to_string(),
&arg.node.arg,
Binding {
kind: BindingKind::Argument,
used: None,
@@ -2016,7 +2109,7 @@ fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> b
};
// Mark the sub-importation as used.
if let Some(binding) = scope.values.get_mut(&alias) {
if let Some(binding) = scope.values.get_mut(alias.as_str()) {
binding.used = Some((scope_id, Range::from_located(expr)));
}
true
@@ -2052,7 +2145,7 @@ impl<'a> Checker<'a> {
for builtin in BUILTINS {
scope.values.insert(
(*builtin).to_string(),
builtin,
Binding {
kind: BindingKind::Builtin,
range: Default::default(),
@@ -2062,7 +2155,7 @@ impl<'a> Checker<'a> {
}
for builtin in MAGIC_GLOBALS {
scope.values.insert(
(*builtin).to_string(),
builtin,
Binding {
kind: BindingKind::Builtin,
range: Default::default(),
@@ -2090,23 +2183,26 @@ impl<'a> Checker<'a> {
}
}
fn add_binding(&mut self, name: String, binding: Binding) {
fn add_binding<'b>(&mut self, name: &'b str, binding: Binding)
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::F402) {
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(existing) = scope.values.get(&name) {
if matches!(binding.kind, BindingKind::LoopVar)
&& matches!(
existing.kind,
BindingKind::Importation(_, _, _)
| BindingKind::FromImportation(_, _, _)
| BindingKind::SubmoduleImportation(_, _, _)
| BindingKind::StarImportation
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::StarImportation(..)
| BindingKind::FutureImportation
)
{
self.add_check(Check::new(
CheckKind::ImportShadowedByLoopVar(
name.clone(),
name.to_string(),
existing.range.location.row(),
),
binding.range,
@@ -2164,16 +2260,19 @@ impl<'a> Checker<'a> {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.to_string());
for binding in scope.values.values() {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
));
}
}
}
from_list.sort();
self.add_check(Check::new(
CheckKind::ImportStarUsage(id.clone(), from_list),
CheckKind::ImportStarUsage(id.to_string(), from_list),
Range::from_located(expr),
));
}
@@ -2204,113 +2303,121 @@ impl<'a> Checker<'a> {
}
}
fn handle_node_store(&mut self, expr: &Expr, parent: &Stmt) {
if let ExprKind::Name { id, .. } = &expr.node {
if self.settings.enabled.contains(&CheckCode::F823) {
let scopes: Vec<&Scope> = self
.scope_stack
.iter()
.map(|index| &self.scopes[*index])
.collect();
if let Some(check) = pyflakes::checks::undefined_local(&scopes, id) {
self.add_check(check);
}
fn handle_node_store<'b>(&mut self, id: &'b str, expr: &Expr, parent: &Stmt)
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::F823) {
let scopes: Vec<&Scope> = self
.scope_stack
.iter()
.map(|index| &self.scopes[*index])
.collect();
if let Some(check) = pyflakes::checks::undefined_local(&scopes, id) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::N806) {
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
if self.settings.enabled.contains(&CheckCode::N806) {
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
// Ignore globals.
if !self
.current_scope()
.values
.get(id)
.map(|binding| matches!(binding.kind, BindingKind::Global))
.unwrap_or(false)
{
pep8_naming::plugins::non_lowercase_variable_in_function(self, expr, parent, id)
}
}
}
if self.settings.enabled.contains(&CheckCode::N815) {
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
if self.settings.enabled.contains(&CheckCode::N815) {
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N816) {
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(
self, expr, parent, id,
)
}
}
if matches!(parent.node, StmtKind::AnnAssign { value: None, .. }) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Annotation,
used: None,
range: Range::from_located(expr),
},
);
return;
}
// TODO(charlie): Include comprehensions here.
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::LoopVar,
used: None,
range: Range::from_located(expr),
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Binding,
used: None,
range: Range::from_located(expr),
},
);
return;
}
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& matches!(
parent.node,
StmtKind::Assign { .. }
| StmtKind::AugAssign { .. }
| StmtKind::AnnAssign { .. }
)
{
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Export(extract_all_names(parent, current)),
used: None,
range: Range::from_located(expr),
},
);
return;
if self.settings.enabled.contains(&CheckCode::N816) {
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(self, expr, parent, id)
}
}
if matches!(parent.node, StmtKind::AnnAssign { value: None, .. }) {
self.add_binding(
id.to_string(),
id,
Binding {
kind: BindingKind::Assignment,
kind: BindingKind::Annotation,
used: None,
range: Range::from_located(expr),
},
);
return;
}
// TODO(charlie): Include comprehensions here.
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) {
self.add_binding(
id,
Binding {
kind: BindingKind::LoopVar,
used: None,
range: Range::from_located(expr),
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id,
Binding {
kind: BindingKind::Binding,
used: None,
range: Range::from_located(expr),
},
);
return;
}
let current = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& matches!(
parent.node,
StmtKind::Assign { .. } | StmtKind::AugAssign { .. } | StmtKind::AnnAssign { .. }
)
{
self.add_binding(
id,
Binding {
kind: BindingKind::Export(extract_all_names(parent, current)),
used: None,
range: Range::from_located(expr),
},
);
return;
}
self.add_binding(
id,
Binding {
kind: BindingKind::Assignment,
used: None,
range: Range::from_located(expr),
},
);
}
fn handle_node_delete(&mut self, expr: &Expr) {
fn handle_node_delete<'b>(&mut self, expr: &'b Expr)
where
'b: 'a,
{
if let ExprKind::Name { id, .. } = &expr.node {
if operations::on_conditional_branch(
&mut self
@@ -2324,10 +2431,11 @@ impl<'a> Checker<'a> {
let scope =
&mut self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if scope.values.remove(id).is_none() && self.settings.enabled.contains(&CheckCode::F821)
if scope.values.remove(&id.as_str()).is_none()
&& self.settings.enabled.contains(&CheckCode::F821)
{
self.add_check(Check::new(
CheckKind::UndefinedName(id.clone()),
CheckKind::UndefinedName(id.to_string()),
Range::from_located(expr),
))
}
@@ -2459,16 +2567,19 @@ impl<'a> Checker<'a> {
let mut checks: Vec<Check> = vec![];
for scope in self.dead_scopes.iter().map(|index| &self.scopes[*index]) {
let all_binding = scope.values.get("__all__");
let all_names = all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names),
_ => None,
});
let all_binding: Option<&Binding> = scope.values.get("__all__");
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some(names.iter().map(|name| name.as_str()).collect())
}
_ => None,
});
if self.settings.enabled.contains(&CheckCode::F822) {
if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
if let Some(names) = &all_names {
for name in names {
if !scope.values.contains_key(name) {
checks.push(Check::new(
@@ -2485,11 +2596,14 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::F405) {
if scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
if let Some(names) = &all_names {
let mut from_list = vec![];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.to_string());
for binding in scope.values.values() {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
));
}
}
from_list.sort();
@@ -2497,7 +2611,10 @@ impl<'a> Checker<'a> {
for name in names {
if !scope.values.contains_key(name) {
checks.push(Check::new(
CheckKind::ImportStarUsage(name.clone(), from_list.clone()),
CheckKind::ImportStarUsage(
name.to_string(),
from_list.clone(),
),
all_binding.range,
));
}
@@ -2514,8 +2631,18 @@ impl<'a> Checker<'a> {
BTreeMap::new();
for (name, binding) in scope.values.iter() {
if !matches!(
binding.kind,
BindingKind::Importation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FromImportation(..)
) {
continue;
}
let used = binding.used.is_some()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default();
@@ -2542,7 +2669,7 @@ impl<'a> Checker<'a> {
.or_default()
.push(full_name);
}
_ => {}
_ => unreachable!("Already filtered on BindingKind."),
}
}
}
@@ -2551,7 +2678,7 @@ impl<'a> Checker<'a> {
let child = self.parents[defined_by];
let parent = defined_in.map(|defined_in| self.parents[defined_in]);
let fix = if self.patch() {
let fix = if self.patch(&CheckCode::F401) {
let deleted: Vec<&Stmt> = self
.deletions
.iter()

View File

@@ -58,10 +58,7 @@ pub fn check_lines(
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
// Enforce unnecessary coding comments (U009).
if enforce_unnecessary_coding_comment {
@@ -76,7 +73,7 @@ pub fn check_lines(
end_location: Location::new(lineno + 1, line_length + 1),
},
);
if autofix.patch() {
if autofix.patch() && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::deletion(
Location::new(lineno + 1, 0),
Location::new(lineno + 1, line_length + 1),
@@ -106,7 +103,7 @@ pub fn check_lines(
matches.push(check.kind.code().as_ref());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
(Directive::Codes(.., codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
@@ -136,7 +133,7 @@ pub fn check_lines(
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
(Directive::Codes(.., codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
@@ -155,10 +152,7 @@ pub fn check_lines(
// want to raise W292 anyway).
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let noqa = noqa_directives
.entry(noqa_lineno)
@@ -176,7 +170,7 @@ pub fn check_lines(
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
(Directive::Codes(.., codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
@@ -192,7 +186,7 @@ pub fn check_lines(
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(start, end) => {
Directive::All(spaces, start, end) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
@@ -201,16 +195,16 @@ pub fn check_lines(
end_location: Location::new(row + 1, end),
},
);
if autofix.patch() {
if autofix.patch() && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::deletion(
Location::new(row + 1, start),
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
}
line_checks.push(check);
}
}
Directive::Codes(start, end, codes) => {
Directive::Codes(spaces, start, end, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
@@ -229,15 +223,15 @@ pub fn check_lines(
end_location: Location::new(row + 1, end),
},
);
if autofix.patch() {
if autofix.patch() && settings.fixable.contains(check.kind.code()) {
if valid_codes.is_empty() {
check.amend(Fix::deletion(
Location::new(row + 1, start),
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
} else {
check.amend(Fix::replacement(
format!(" # noqa: {}", valid_codes.join(", ")),
format!("# noqa: {}", valid_codes.join(", ")),
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));

View File

@@ -36,7 +36,7 @@ pub fn check_tokens(
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment) {
for check in rules::checks::ambiguous_unicode_character(
checks.extend(rules::checks::ambiguous_unicode_character(
locator,
start,
end,
@@ -49,12 +49,9 @@ pub fn check_tokens(
} else {
Context::Comment
},
autofix.patch(),
) {
if settings.enabled.contains(check.kind.code()) {
checks.push(check);
}
}
settings,
autofix,
));
}
}

View File

@@ -8,6 +8,7 @@ use strum_macros::{AsRefStr, EnumIter, EnumString};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::pyupgrade::types::Primitive;
#[derive(
@@ -102,6 +103,8 @@ pub enum CheckCode {
B025,
B026,
B027,
// flake8-blind-except
BLE001,
// flake8-comprehensions
C400,
C401,
@@ -119,6 +122,10 @@ pub enum CheckCode {
C415,
C416,
C417,
// mccabe
C901,
// flake8-tidy-imports
I252,
// flake8-print
T201,
T203,
@@ -152,7 +159,6 @@ pub enum CheckCode {
YTT303,
// pyupgrade
U001,
U002,
U003,
U004,
U005,
@@ -163,6 +169,8 @@ pub enum CheckCode {
U010,
U011,
U012,
U013,
U014,
// pydocstyle
D100,
D101,
@@ -233,6 +241,10 @@ pub enum CheckCode {
S105,
S106,
S107,
// flake8-boolean-trap
FBT001,
FBT002,
FBT003,
// Ruff
RUF001,
RUF002,
@@ -251,12 +263,16 @@ pub enum CheckCategory {
PEP8Naming,
Flake8Bandit,
Flake8Comprehensions,
Flake8BooleanTrap,
Flake8Bugbear,
Flake8Builtins,
Flake8TidyImports,
Flake8Print,
Flake8Quotes,
Flake8Annotations,
Flake82020,
Flake8BlindExcept,
McCabe,
Ruff,
Meta,
}
@@ -268,16 +284,20 @@ impl CheckCategory {
CheckCategory::Pyflakes => "Pyflakes",
CheckCategory::Isort => "isort",
CheckCategory::Flake8Bandit => "flake8-bandit",
CheckCategory::Flake8BooleanTrap => "flake8-boolean-trap",
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Annotations => "flake8-annotations",
CheckCategory::Flake82020 => "flake8-2020",
CheckCategory::Flake8BlindExcept => "flake8-blind-except",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
CheckCategory::McCabe => "mccabe",
CheckCategory::Ruff => "Ruff-specific rules",
CheckCategory::Meta => "Meta rules",
}
@@ -297,6 +317,9 @@ impl CheckCategory {
CheckCategory::Flake8Comprehensions => {
Some("https://pypi.org/project/flake8-comprehensions/3.10.1/")
}
CheckCategory::Flake8TidyImports => {
Some("https://pypi.org/project/flake8-tidy-imports/4.8.0/")
}
CheckCategory::Flake8Print => Some("https://pypi.org/project/flake8-print/5.0.0/"),
CheckCategory::Flake8Quotes => Some("https://pypi.org/project/flake8-quotes/3.3.1/"),
CheckCategory::Flake8Annotations => {
@@ -307,6 +330,13 @@ impl CheckCategory {
CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"),
CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"),
CheckCategory::Flake8Bandit => Some("https://pypi.org/project/flake8-bandit/4.1.1/"),
CheckCategory::Flake8BlindExcept => {
Some("https://pypi.org/project/flake8-blind-except/0.2.1/")
}
CheckCategory::McCabe => Some("https://pypi.org/project/mccabe/0.7.0/"),
CheckCategory::Flake8BooleanTrap => {
Some("https://pypi.org/project/flake8-boolean-trap/0.1.0/")
}
CheckCategory::Ruff => None,
CheckCategory::Meta => None,
}
@@ -381,6 +411,8 @@ pub enum CheckKind {
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-blind-except
BlindExcept,
// flake8-bugbear
UnaryPrefixIncrement,
AssignmentToOsEnviron,
@@ -424,6 +456,8 @@ pub enum CheckKind {
UnnecessarySubscriptReversal(String),
UnnecessaryComprehension(String),
UnnecessaryMap(String),
// flake8-tidy-imports
BannedRelativeImport(Strictness),
// flake8-print
PrintFound,
PPrintFound,
@@ -457,7 +491,6 @@ pub enum CheckKind {
SysVersionSlice1Referenced,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
UselessMetaclassType,
DeprecatedUnittestAlias(String, String),
UselessObjectInheritance(String),
@@ -468,6 +501,8 @@ pub enum CheckKind {
UnnecessaryFutureImport(Vec<String>),
UnnecessaryLRUCacheParams,
UnnecessaryEncodeUTF8,
ConvertTypedDictFunctionalToClass(String),
ConvertNamedTupleFunctionalToClass(String),
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -538,6 +573,12 @@ pub enum CheckKind {
HardcodedPasswordString(String),
HardcodedPasswordFuncArg(String),
HardcodedPasswordDefault(String),
// mccabe
FunctionIsTooComplex(String, usize),
// flake8-boolean-trap
BooleanPositionalArgInFunctionDefinition,
BooleanDefaultValueInFunctionDefinition,
BooleanPositionalValueInFunctionCall,
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -684,6 +725,8 @@ impl CheckCode {
}
CheckCode::C416 => CheckKind::UnnecessaryComprehension("(list|set)".to_string()),
CheckCode::C417 => CheckKind::UnnecessaryMap("(list|set|dict)".to_string()),
// flake8-tidy-imports
CheckCode::I252 => CheckKind::BannedRelativeImport(Strictness::All),
// flake8-print
CheckCode::T201 => CheckKind::PrintFound,
CheckCode::T203 => CheckKind::PPrintFound,
@@ -715,9 +758,10 @@ impl CheckCode {
CheckCode::YTT301 => CheckKind::SysVersion0Referenced,
CheckCode::YTT302 => CheckKind::SysVersionCmpStr10,
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// flake8-blind-except
CheckCode::BLE001 => CheckKind::BlindExcept,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
CheckCode::U003 => CheckKind::TypeOfPrimitive(Primitive::Str),
CheckCode::U004 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::U005 => CheckKind::DeprecatedUnittestAlias(
@@ -731,6 +775,8 @@ impl CheckCode {
CheckCode::U010 => CheckKind::UnnecessaryFutureImport(vec!["...".to_string()]),
CheckCode::U011 => CheckKind::UnnecessaryLRUCacheParams,
CheckCode::U012 => CheckKind::UnnecessaryEncodeUTF8,
CheckCode::U013 => CheckKind::ConvertTypedDictFunctionalToClass("...".to_string()),
CheckCode::U014 => CheckKind::ConvertNamedTupleFunctionalToClass("...".to_string()),
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -817,6 +863,11 @@ impl CheckCode {
CheckCode::S105 => CheckKind::HardcodedPasswordString("...".to_string()),
CheckCode::S106 => CheckKind::HardcodedPasswordFuncArg("...".to_string()),
CheckCode::S107 => CheckKind::HardcodedPasswordDefault("...".to_string()),
CheckCode::C901 => CheckKind::FunctionIsTooComplex("...".to_string(), 10),
// flake8-boolean-trap
CheckCode::FBT001 => CheckKind::BooleanPositionalArgInFunctionDefinition,
CheckCode::FBT002 => CheckKind::BooleanDefaultValueInFunctionDefinition,
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -900,6 +951,7 @@ impl CheckCode {
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
CheckCode::C402 => CheckCategory::Flake8Comprehensions,
@@ -916,6 +968,7 @@ impl CheckCode {
CheckCode::C415 => CheckCategory::Flake8Comprehensions,
CheckCode::C416 => CheckCategory::Flake8Comprehensions,
CheckCode::C417 => CheckCategory::Flake8Comprehensions,
CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
CheckCode::Q000 => CheckCategory::Flake8Quotes,
@@ -944,7 +997,6 @@ impl CheckCode {
CheckCode::YTT302 => CheckCategory::Flake82020,
CheckCode::YTT303 => CheckCategory::Flake82020,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
CheckCode::U004 => CheckCategory::Pyupgrade,
CheckCode::U005 => CheckCategory::Pyupgrade,
@@ -955,6 +1007,8 @@ impl CheckCode {
CheckCode::U010 => CheckCategory::Pyupgrade,
CheckCode::U011 => CheckCategory::Pyupgrade,
CheckCode::U012 => CheckCategory::Pyupgrade,
CheckCode::U013 => CheckCategory::Pyupgrade,
CheckCode::U014 => CheckCategory::Pyupgrade,
CheckCode::D100 => CheckCategory::Pydocstyle,
CheckCode::D101 => CheckCategory::Pydocstyle,
CheckCode::D102 => CheckCategory::Pydocstyle,
@@ -1021,6 +1075,10 @@ impl CheckCode {
CheckCode::S105 => CheckCategory::Flake8Bandit,
CheckCode::S106 => CheckCategory::Flake8Bandit,
CheckCode::S107 => CheckCategory::Flake8Bandit,
CheckCode::C901 => CheckCategory::McCabe,
CheckCode::FBT001 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::RUF001 => CheckCategory::Ruff,
CheckCode::RUF002 => CheckCategory::Ruff,
CheckCode::RUF003 => CheckCategory::Ruff,
@@ -1109,6 +1167,8 @@ impl CheckKind {
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
// flake8-blind-except
CheckKind::BlindExcept => &CheckCode::BLE001,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -1126,6 +1186,8 @@ impl CheckKind {
CheckKind::UnnecessarySubscriptReversal(_) => &CheckCode::C415,
CheckKind::UnnecessaryComprehension(..) => &CheckCode::C416,
CheckKind::UnnecessaryMap(_) => &CheckCode::C417,
// flake8-tidy-imports
CheckKind::BannedRelativeImport(_) => &CheckCode::I252,
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
@@ -1159,7 +1221,6 @@ impl CheckKind {
CheckKind::SysVersionSlice1Referenced => &CheckCode::YTT303,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
CheckKind::UselessMetaclassType => &CheckCode::U001,
CheckKind::DeprecatedUnittestAlias(..) => &CheckCode::U005,
CheckKind::UsePEP585Annotation(_) => &CheckCode::U006,
@@ -1170,6 +1231,8 @@ impl CheckKind {
CheckKind::UnnecessaryFutureImport(_) => &CheckCode::U010,
CheckKind::UnnecessaryLRUCacheParams => &CheckCode::U011,
CheckKind::UnnecessaryEncodeUTF8 => &CheckCode::U012,
CheckKind::ConvertTypedDictFunctionalToClass(_) => &CheckCode::U013,
CheckKind::ConvertNamedTupleFunctionalToClass(_) => &CheckCode::U014,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1240,6 +1303,12 @@ impl CheckKind {
CheckKind::HardcodedPasswordString(..) => &CheckCode::S105,
CheckKind::HardcodedPasswordFuncArg(..) => &CheckCode::S106,
CheckKind::HardcodedPasswordDefault(..) => &CheckCode::S107,
// McCabe
CheckKind::FunctionIsTooComplex(..) => &CheckCode::C901,
// flake8-boolean-trap
CheckKind::BooleanPositionalArgInFunctionDefinition => &CheckCode::FBT001,
CheckKind::BooleanDefaultValueInFunctionDefinition => &CheckCode::FBT002,
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1579,6 +1648,13 @@ impl CheckKind {
format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)")
}
}
// flake8-tidy-imports
CheckKind::BannedRelativeImport(strictness) => match strictness {
Strictness::Parents => {
"Relative imports from parent modules are banned".to_string()
}
Strictness::All => "Relative imports are banned".to_string(),
},
// flake8-print
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
@@ -1674,9 +1750,6 @@ impl CheckKind {
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
}
CheckKind::UnnecessaryAbspath => {
"`abspath(__file__)` is unnecessary in Python 3.9 and later".to_string()
}
CheckKind::UselessMetaclassType => "`__metaclass__ = type` is implied".to_string(),
CheckKind::DeprecatedUnittestAlias(alias, target) => {
format!("`{alias}` is deprecated, use `{target}` instead")
@@ -1708,6 +1781,12 @@ impl CheckKind {
"Unnecessary parameters to `functools.lru_cache`".to_string()
}
CheckKind::UnnecessaryEncodeUTF8 => "Unnecessary call to `encode` as UTF-8".to_string(),
CheckKind::ConvertTypedDictFunctionalToClass(name) => {
format!("Convert `{name}` from `TypedDict` functional to class syntax")
}
CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
format!("Convert `{name}` from `NamedTuple` functional to class syntax")
}
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::BlankLineAfterSummary => {
@@ -1888,6 +1967,22 @@ impl CheckKind {
CheckKind::HardcodedPasswordDefault(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
// flake8-blind-except
CheckKind::BlindExcept => "Blind except Exception: statement".to_string(),
// McCabe
CheckKind::FunctionIsTooComplex(name, complexity) => {
format!("`{name}` is too complex ({complexity})")
}
// flake8-boolean-trap
CheckKind::BooleanPositionalArgInFunctionDefinition => {
"Boolean positional arg in function definition".to_string()
}
CheckKind::BooleanDefaultValueInFunctionDefinition => {
"Boolean default value in function definition".to_string()
}
CheckKind::BooleanPositionalValueInFunctionCall => {
"Boolean positional value in function call".to_string()
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -1951,46 +2046,53 @@ impl CheckKind {
pub fn fixable(&self) -> bool {
matches!(
self,
CheckKind::AmbiguousUnicodeCharacterString(_, _)
| CheckKind::AmbiguousUnicodeCharacterDocstring(_, _)
| CheckKind::BlankLineAfterLastSection(_)
| CheckKind::BlankLineAfterSection(_)
CheckKind::AmbiguousUnicodeCharacterString(..)
| CheckKind::AmbiguousUnicodeCharacterDocstring(..)
| CheckKind::BlankLineAfterLastSection(..)
| CheckKind::BlankLineAfterSection(..)
| CheckKind::BlankLineAfterSummary
| CheckKind::BlankLineBeforeSection(_)
| CheckKind::CapitalizeSectionName(_)
| CheckKind::DashedUnderlineAfterSection(_)
| CheckKind::DeprecatedUnittestAlias(_, _)
| CheckKind::BlankLineBeforeSection(..)
| CheckKind::CapitalizeSectionName(..)
| CheckKind::ConvertNamedTupleFunctionalToClass(..)
| CheckKind::ConvertTypedDictFunctionalToClass(..)
| CheckKind::DashedUnderlineAfterSection(..)
| CheckKind::DeprecatedUnittestAlias(..)
| CheckKind::DoNotAssertFalse
| CheckKind::DuplicateHandlerException(_)
| CheckKind::DoNotAssignLambda
| CheckKind::DuplicateHandlerException(..)
| CheckKind::GetAttrWithConstant
| CheckKind::IsLiteral
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(_)
| CheckKind::NoBlankLineAfterFunction(_)
| CheckKind::NoBlankLineBeforeClass(_)
| CheckKind::NoBlankLineBeforeFunction(_)
| CheckKind::NoBlankLinesBetweenHeaderAndContent(_)
| CheckKind::NewLineAfterSectionName(..)
| CheckKind::NoBlankLineAfterFunction(..)
| CheckKind::NoBlankLineBeforeClass(..)
| CheckKind::NoBlankLineBeforeFunction(..)
| CheckKind::NoBlankLinesBetweenHeaderAndContent(..)
| CheckKind::NoOverIndentation
| CheckKind::NoSurroundingWhitespace
| CheckKind::NoUnderIndentation
| CheckKind::OneBlankLineAfterClass(_)
| CheckKind::OneBlankLineBeforeClass(_)
| CheckKind::NoneComparison(..)
| CheckKind::NotInTest
| CheckKind::NotIsTest
| CheckKind::OneBlankLineAfterClass(..)
| CheckKind::OneBlankLineBeforeClass(..)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::PPrintFound
| CheckKind::PrintFound
| CheckKind::RaiseNotImplemented
| CheckKind::SectionNameEndsInColon(_)
| CheckKind::SectionNotOverIndented(_)
| CheckKind::SectionUnderlineAfterName(_)
| CheckKind::SectionUnderlineMatchesSectionLength(_)
| CheckKind::SectionUnderlineNotOverIndented(_)
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
| CheckKind::SectionUnderlineMatchesSectionLength(..)
| CheckKind::SectionUnderlineNotOverIndented(..)
| CheckKind::SetAttrWithConstant
| CheckKind::SuperCallWithParameters
| CheckKind::TypeOfPrimitive(_)
| CheckKind::UnnecessaryAbspath
| CheckKind::UnnecessaryCollectionCall(_)
| CheckKind::UnnecessaryComprehension(_)
| CheckKind::TrueFalseComparison(..)
| CheckKind::TypeOfPrimitive(..)
| CheckKind::UnnecessaryCollectionCall(..)
| CheckKind::UnnecessaryComprehension(..)
| CheckKind::UnnecessaryEncodeUTF8
| CheckKind::UnnecessaryFutureImport(_)
| CheckKind::UnnecessaryFutureImport(..)
| CheckKind::UnnecessaryGeneratorDict
| CheckKind::UnnecessaryGeneratorList
| CheckKind::UnnecessaryGeneratorSet
@@ -1998,18 +2100,18 @@ impl CheckKind {
| CheckKind::UnnecessaryListCall
| CheckKind::UnnecessaryListComprehensionDict
| CheckKind::UnnecessaryListComprehensionSet
| CheckKind::UnnecessaryLiteralDict(_)
| CheckKind::UnnecessaryLiteralSet(_)
| CheckKind::UnnecessaryLiteralWithinListCall(_)
| CheckKind::UnnecessaryLiteralWithinTupleCall(_)
| CheckKind::UnnecessaryLiteralDict(..)
| CheckKind::UnnecessaryLiteralSet(..)
| CheckKind::UnnecessaryLiteralWithinListCall(..)
| CheckKind::UnnecessaryLiteralWithinTupleCall(..)
| CheckKind::UnsortedImports
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedLoopControlVariable(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::UsePEP585Annotation(_)
| CheckKind::UnusedLoopControlVariable(..)
| CheckKind::UnusedNOQA(..)
| CheckKind::UsePEP585Annotation(..)
| CheckKind::UsePEP604Annotation
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(_)
| CheckKind::UselessObjectInheritance(..)
)
}
}

View File

@@ -63,6 +63,10 @@ pub enum CheckCodePrefix {
B025,
B026,
B027,
BLE,
BLE0,
BLE00,
BLE001,
C,
C4,
C40,
@@ -83,6 +87,9 @@ pub enum CheckCodePrefix {
C415,
C416,
C417,
C9,
C90,
C901,
D,
D1,
D10,
@@ -211,10 +218,19 @@ pub enum CheckCodePrefix {
F9,
F90,
F901,
FBT,
FBT0,
FBT00,
FBT001,
FBT002,
FBT003,
I,
I0,
I00,
I001,
I2,
I25,
I252,
M,
M0,
M00,
@@ -269,7 +285,6 @@ pub enum CheckCodePrefix {
U0,
U00,
U001,
U002,
U003,
U004,
U005,
@@ -281,6 +296,8 @@ pub enum CheckCodePrefix {
U010,
U011,
U012,
U013,
U014,
W,
W2,
W29,
@@ -478,6 +495,10 @@ impl CheckCodePrefix {
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE001 => vec![CheckCode::BLE001],
CheckCodePrefix::C => vec![
CheckCode::C400,
CheckCode::C401,
@@ -495,6 +516,7 @@ impl CheckCodePrefix {
CheckCode::C415,
CheckCode::C416,
CheckCode::C417,
CheckCode::C901,
],
CheckCodePrefix::C4 => vec![
CheckCode::C400,
@@ -550,6 +572,9 @@ impl CheckCodePrefix {
CheckCodePrefix::C415 => vec![CheckCode::C415],
CheckCodePrefix::C416 => vec![CheckCode::C416],
CheckCodePrefix::C417 => vec![CheckCode::C417],
CheckCodePrefix::C9 => vec![CheckCode::C901],
CheckCodePrefix::C90 => vec![CheckCode::C901],
CheckCodePrefix::C901 => vec![CheckCode::C901],
CheckCodePrefix::D => vec![
CheckCode::D100,
CheckCode::D101,
@@ -926,10 +951,19 @@ impl CheckCodePrefix {
CheckCodePrefix::F9 => vec![CheckCode::F901],
CheckCodePrefix::F90 => vec![CheckCode::F901],
CheckCodePrefix::F901 => vec![CheckCode::F901],
CheckCodePrefix::I => vec![CheckCode::I001],
CheckCodePrefix::FBT => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003],
CheckCodePrefix::FBT0 => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003],
CheckCodePrefix::FBT00 => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003],
CheckCodePrefix::FBT001 => vec![CheckCode::FBT001],
CheckCodePrefix::FBT002 => vec![CheckCode::FBT002],
CheckCodePrefix::FBT003 => vec![CheckCode::FBT003],
CheckCodePrefix::I => vec![CheckCode::I252, CheckCode::I001],
CheckCodePrefix::I0 => vec![CheckCode::I001],
CheckCodePrefix::I00 => vec![CheckCode::I001],
CheckCodePrefix::I001 => vec![CheckCode::I001],
CheckCodePrefix::I2 => vec![CheckCode::I252],
CheckCodePrefix::I25 => vec![CheckCode::I252],
CheckCodePrefix::I252 => vec![CheckCode::I252],
CheckCodePrefix::M => vec![CheckCode::M001],
CheckCodePrefix::M0 => vec![CheckCode::M001],
CheckCodePrefix::M00 => vec![CheckCode::M001],
@@ -1067,7 +1101,6 @@ impl CheckCodePrefix {
CheckCodePrefix::T203 => vec![CheckCode::T203],
CheckCodePrefix::U => vec![
CheckCode::U001,
CheckCode::U002,
CheckCode::U003,
CheckCode::U004,
CheckCode::U005,
@@ -1078,10 +1111,11 @@ impl CheckCodePrefix {
CheckCode::U010,
CheckCode::U011,
CheckCode::U012,
CheckCode::U013,
CheckCode::U014,
],
CheckCodePrefix::U0 => vec![
CheckCode::U001,
CheckCode::U002,
CheckCode::U003,
CheckCode::U004,
CheckCode::U005,
@@ -1092,10 +1126,11 @@ impl CheckCodePrefix {
CheckCode::U010,
CheckCode::U011,
CheckCode::U012,
CheckCode::U013,
CheckCode::U014,
],
CheckCodePrefix::U00 => vec![
CheckCode::U001,
CheckCode::U002,
CheckCode::U003,
CheckCode::U004,
CheckCode::U005,
@@ -1105,7 +1140,6 @@ impl CheckCodePrefix {
CheckCode::U009,
],
CheckCodePrefix::U001 => vec![CheckCode::U001],
CheckCodePrefix::U002 => vec![CheckCode::U002],
CheckCodePrefix::U003 => vec![CheckCode::U003],
CheckCodePrefix::U004 => vec![CheckCode::U004],
CheckCodePrefix::U005 => vec![CheckCode::U005],
@@ -1113,10 +1147,18 @@ impl CheckCodePrefix {
CheckCodePrefix::U007 => vec![CheckCode::U007],
CheckCodePrefix::U008 => vec![CheckCode::U008],
CheckCodePrefix::U009 => vec![CheckCode::U009],
CheckCodePrefix::U01 => vec![CheckCode::U010, CheckCode::U011, CheckCode::U012],
CheckCodePrefix::U01 => vec![
CheckCode::U010,
CheckCode::U011,
CheckCode::U012,
CheckCode::U013,
CheckCode::U014,
],
CheckCodePrefix::U010 => vec![CheckCode::U010],
CheckCodePrefix::U011 => vec![CheckCode::U011],
CheckCodePrefix::U012 => vec![CheckCode::U012],
CheckCodePrefix::U013 => vec![CheckCode::U013],
CheckCodePrefix::U014 => vec![CheckCode::U014],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -1225,6 +1267,10 @@ impl CheckCodePrefix {
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
CheckCodePrefix::BLE => PrefixSpecificity::Category,
CheckCodePrefix::BLE0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::BLE00 => PrefixSpecificity::Tens,
CheckCodePrefix::BLE001 => PrefixSpecificity::Explicit,
CheckCodePrefix::C => PrefixSpecificity::Category,
CheckCodePrefix::C4 => PrefixSpecificity::Hundreds,
CheckCodePrefix::C40 => PrefixSpecificity::Tens,
@@ -1245,6 +1291,9 @@ impl CheckCodePrefix {
CheckCodePrefix::C415 => PrefixSpecificity::Explicit,
CheckCodePrefix::C416 => PrefixSpecificity::Explicit,
CheckCodePrefix::C417 => PrefixSpecificity::Explicit,
CheckCodePrefix::C9 => PrefixSpecificity::Hundreds,
CheckCodePrefix::C90 => PrefixSpecificity::Tens,
CheckCodePrefix::C901 => PrefixSpecificity::Explicit,
CheckCodePrefix::D => PrefixSpecificity::Category,
CheckCodePrefix::D1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::D10 => PrefixSpecificity::Tens,
@@ -1373,10 +1422,19 @@ impl CheckCodePrefix {
CheckCodePrefix::F9 => PrefixSpecificity::Hundreds,
CheckCodePrefix::F90 => PrefixSpecificity::Tens,
CheckCodePrefix::F901 => PrefixSpecificity::Explicit,
CheckCodePrefix::FBT => PrefixSpecificity::Category,
CheckCodePrefix::FBT0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::FBT00 => PrefixSpecificity::Tens,
CheckCodePrefix::FBT001 => PrefixSpecificity::Explicit,
CheckCodePrefix::FBT002 => PrefixSpecificity::Explicit,
CheckCodePrefix::FBT003 => PrefixSpecificity::Explicit,
CheckCodePrefix::I => PrefixSpecificity::Category,
CheckCodePrefix::I0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::I00 => PrefixSpecificity::Tens,
CheckCodePrefix::I001 => PrefixSpecificity::Explicit,
CheckCodePrefix::I2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::I25 => PrefixSpecificity::Tens,
CheckCodePrefix::I252 => PrefixSpecificity::Explicit,
CheckCodePrefix::M => PrefixSpecificity::Category,
CheckCodePrefix::M0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::M00 => PrefixSpecificity::Tens,
@@ -1431,7 +1489,6 @@ impl CheckCodePrefix {
CheckCodePrefix::U0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::U00 => PrefixSpecificity::Tens,
CheckCodePrefix::U001 => PrefixSpecificity::Explicit,
CheckCodePrefix::U002 => PrefixSpecificity::Explicit,
CheckCodePrefix::U003 => PrefixSpecificity::Explicit,
CheckCodePrefix::U004 => PrefixSpecificity::Explicit,
CheckCodePrefix::U005 => PrefixSpecificity::Explicit,
@@ -1443,6 +1500,8 @@ impl CheckCodePrefix {
CheckCodePrefix::U010 => PrefixSpecificity::Explicit,
CheckCodePrefix::U011 => PrefixSpecificity::Explicit,
CheckCodePrefix::U012 => PrefixSpecificity::Explicit,
CheckCodePrefix::U013 => PrefixSpecificity::Explicit,
CheckCodePrefix::U014 => PrefixSpecificity::Explicit,
CheckCodePrefix::W => PrefixSpecificity::Category,
CheckCodePrefix::W2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W29 => PrefixSpecificity::Tens,

View File

@@ -1,19 +1,16 @@
use std::fmt;
use std::path::PathBuf;
use clap::{command, Parser};
use fnv::FnvHashMap;
use log::warn;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::logging::LogLevel;
use crate::printer::SerializationFormat;
use crate::settings::configuration::Configuration;
use crate::settings::types::{PatternPrefixPair, PerFileIgnore, PythonVersion};
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(author, about = "Ruff: An extremely fast Python linter.")]
#[command(version)]
pub struct Cli {
#[arg(required = true)]
@@ -66,16 +63,27 @@ pub struct Cli {
/// excluded ones.
#[arg(long, value_delimiter = ',')]
pub extend_exclude: Vec<String>,
/// List of error codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
pub fixable: Vec<CheckCodePrefix>,
/// List of error codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
pub unfixable: Vec<CheckCodePrefix>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Vec<PatternPrefixPair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
pub format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
/// Show violations with source code.
#[arg(long)]
pub show_source: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See ruff's settings.
/// See Ruff's settings.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
@@ -91,6 +99,9 @@ pub struct Cli {
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Max McCabe complexity allowed for a function.
#[arg(long)]
pub max_complexity: Option<usize>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
@@ -131,68 +142,10 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
}
}
pub enum Warnable {
Select,
ExtendSelect,
}
impl fmt::Display for Warnable {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Warnable::Select => fmt.write_str("--select"),
Warnable::ExtendSelect => fmt.write_str("--extend-select"),
}
}
}
/// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on(
flag: Warnable,
codes: &[CheckCodePrefix],
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_configuration: &Configuration,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
if !cli_ignore.is_empty() {
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_configuration.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!("{code:?} was passed to {flag}, but ignored by the default `ignore` field",)
}
}
if !cli_extend_ignore.is_empty() {
if cli_extend_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
}
} else if pyproject_configuration.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!(
"{code:?} was passed to {flag}, but ignored by the default `extend_ignore` \
field"
)
}
}
}
}
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
project_root: &Option<PathBuf>,
project_root: Option<&PathBuf>,
) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {

View File

@@ -55,18 +55,14 @@ impl SourceGenerator {
}
fn newline(&mut self) -> fmt::Result {
if self.initial {
self.initial = false;
} else {
if !self.initial {
self.new_lines = std::cmp::max(self.new_lines, 1);
}
Ok(())
}
fn newlines(&mut self, extra: usize) -> fmt::Result {
if self.initial {
self.initial = false;
} else {
if !self.initial {
self.new_lines = std::cmp::max(self.new_lines, 1 + extra);
}
Ok(())
@@ -121,6 +117,7 @@ impl SourceGenerator {
self.newline()?;
self.p(&" ".repeat(self.indentation))?;
$body
self.initial = false;
}};
}
@@ -145,12 +142,11 @@ impl SourceGenerator {
self.unparse_expr(returns, precedence::EXPR)?;
}
self.p(":")?;
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
})
});
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
}
StmtKind::AsyncFunctionDef {
name,
@@ -172,11 +168,11 @@ impl SourceGenerator {
self.unparse_expr(returns, precedence::EXPR)?;
}
self.p(":")?;
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
})
});
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
}
StmtKind::ClassDef {
name,
@@ -209,11 +205,11 @@ impl SourceGenerator {
}
self.p_if(!first, ")")?;
self.p(":")?;
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
})
});
self.body(body)?;
if self.indentation == 0 {
self.newlines(2)?;
}
}
StmtKind::Return { value } => {
statement!({
@@ -299,14 +295,14 @@ impl SourceGenerator {
self.p(" in ")?;
self.unparse_expr(iter, precedence::TEST)?;
self.p(":")?;
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
self.body(orelse)?;
});
}
})
});
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
});
self.body(orelse)?;
}
}
StmtKind::AsyncFor {
target,
@@ -321,59 +317,59 @@ impl SourceGenerator {
self.p(" in ")?;
self.unparse_expr(iter, precedence::TEST)?;
self.p(":")?;
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
self.body(orelse)?;
});
}
})
});
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
});
self.body(orelse)?;
}
}
StmtKind::While { test, body, orelse } => {
statement!({
self.p("while ")?;
self.unparse_expr(test, precedence::TEST)?;
self.p(":")?;
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
self.body(orelse)?;
});
}
})
});
self.body(body)?;
if !orelse.is_empty() {
statement!({
self.p("else:")?;
});
self.body(orelse)?;
}
}
StmtKind::If { test, body, orelse } => {
statement!({
self.p("if ")?;
self.unparse_expr(test, precedence::TEST)?;
self.p(":")?;
self.body(body)?;
let mut orelse_: &Vec<Stmt<U>> = orelse;
loop {
if orelse_.len() == 1 && matches!(orelse_[0].node, StmtKind::If { .. }) {
if let StmtKind::If { body, test, orelse } = &orelse_[0].node {
statement!({
self.p("elif ")?;
self.unparse_expr(test, precedence::TEST)?;
self.p(":")?;
self.body(body)?;
});
orelse_ = orelse;
}
} else {
if !orelse_.is_empty() {
statement!({
self.p("else:")?;
self.body(orelse_)?;
});
}
break;
}
}
});
self.body(body)?;
let mut orelse_: &Vec<Stmt<U>> = orelse;
loop {
if orelse_.len() == 1 && matches!(orelse_[0].node, StmtKind::If { .. }) {
if let StmtKind::If { body, test, orelse } = &orelse_[0].node {
statement!({
self.p("elif ")?;
self.unparse_expr(test, precedence::TEST)?;
self.p(":")?;
});
self.body(body)?;
orelse_ = orelse;
}
} else {
if !orelse_.is_empty() {
statement!({
self.p("else:")?;
});
self.body(orelse_)?;
}
break;
}
}
}
StmtKind::With { items, body, .. } => {
statement!({
@@ -384,8 +380,8 @@ impl SourceGenerator {
self.unparse_withitem(item)?;
}
self.p(":")?;
self.body(body)?;
})
});
self.body(body)?;
}
StmtKind::AsyncWith { items, body, .. } => {
statement!({
@@ -396,8 +392,8 @@ impl SourceGenerator {
self.unparse_withitem(item)?;
}
self.p(":")?;
self.body(body)?;
})
});
self.body(body)?;
}
StmtKind::Match { .. } => {}
StmtKind::Raise { exc, cause } => {
@@ -421,27 +417,27 @@ impl SourceGenerator {
} => {
statement!({
self.p("try:")?;
self.body(body)?;
});
self.body(body)?;
for handler in handlers {
statement!({
self.unparse_excepthandler(handler)?;
});
}
for handler in handlers {
statement!({
self.unparse_excepthandler(handler)?;
});
}
if !orelse.is_empty() {
statement!({
self.p("else:")?;
self.body(orelse)?;
});
}
if !finalbody.is_empty() {
statement!({
self.p("finally:")?;
self.body(finalbody)?;
});
}
})
if !orelse.is_empty() {
statement!({
self.p("else:")?;
});
self.body(orelse)?;
}
if !finalbody.is_empty() {
statement!({
self.p("finally:")?;
});
self.body(finalbody)?;
}
}
StmtKind::Assert { test, msg } => {
statement!({

View File

@@ -0,0 +1,5 @@
pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
"ur\"\"\"", "ur'''", "u\"\"\"", "u'''", "r\"\"\"", "r'''", "\"\"\"", "'''",
];
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &["ur\"", "ur'", "u\"", "u'", "r\"", "r'", "\"", "'"];

View File

@@ -1,7 +1,7 @@
pub mod constants;
pub mod definition;
pub mod extraction;
pub mod google;
pub mod helpers;
pub mod numpy;
pub mod sections;
pub mod styles;

View File

@@ -1,4 +1,4 @@
use crate::docstrings::helpers;
use crate::ast::whitespace;
use crate::docstrings::styles::SectionStyle;
#[derive(Debug)]
@@ -14,7 +14,7 @@ pub(crate) struct SectionContext<'a> {
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
style
.lowercase_section_names()
.contains(&helpers::leading_words(line).to_lowercase().as_str())
.contains(&whitespace::leading_words(line).to_lowercase().as_str())
}
/// Check if the suspected context is really a section header.
@@ -64,7 +64,7 @@ pub(crate) fn section_contexts<'a>(
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: helpers::leading_words(lines[lineno]),
section_name: whitespace::leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],

View File

@@ -1,3 +1,5 @@
use std::ops::Deref;
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
@@ -11,7 +13,7 @@ use crate::{visibility, Check};
#[derive(Default)]
struct ReturnStatementVisitor<'a> {
returns: Vec<&'a Option<Box<Expr>>>,
returns: Vec<Option<&'a Expr>>,
}
impl<'a, 'b> Visitor<'b> for ReturnStatementVisitor<'a>
@@ -23,7 +25,9 @@ where
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// No recurse.
}
StmtKind::Return { value } => self.returns.push(value),
StmtKind::Return { value } => {
self.returns.push(value.as_ref().map(|expr| expr.deref()))
}
_ => visitor::walk_stmt(self, stmt),
}
}

View File

@@ -0,0 +1 @@
pub mod plugins;

View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn blind_except(checker: &mut Checker, handlers: &[Excepthandler]) {
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if let Some(type_) = type_ {
if let ExprKind::Name { id, .. } = &type_.node {
for exception in ["BaseException", "Exception"] {
if id == exception {
checker.add_check(Check::new(
CheckKind::BlindExcept,
Range::from_located(type_),
));
}
}
}
}
}
}

View File

@@ -0,0 +1 @@
pub mod plugins;

View File

@@ -0,0 +1,71 @@
use rustpython_ast::{Arguments, ExprKind};
use rustpython_parser::ast::{Constant, Expr};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn is_boolean_arg(arg: &Expr) -> bool {
matches!(
&arg.node,
ExprKind::Constant {
value: Constant::Bool(_),
..
}
)
}
fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: CheckKind) {
if is_boolean_arg(arg) {
checker.add_check(Check::new(kind, Range::from_located(arg)));
}
}
pub fn check_positional_boolean_in_def(checker: &mut Checker, arguments: &Arguments) {
for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) {
if arg.node.annotation.is_none() {
continue;
}
if let Some(expr) = &arg.node.annotation {
// check for both bool (python class) and 'bool' (string annotation)
let hint = match &expr.node {
ExprKind::Name { id, .. } => id == "bool",
ExprKind::Constant {
value: Constant::Str(value),
..
} => value == "bool",
_ => false,
};
if hint {
checker.add_check(Check::new(
CheckKind::BooleanPositionalArgInFunctionDefinition,
Range::from_located(arg),
));
}
}
}
}
pub fn check_boolean_default_value_in_function_definition(
checker: &mut Checker,
arguments: &Arguments,
) {
for arg in arguments.defaults.iter() {
add_if_boolean(
checker,
arg,
CheckKind::BooleanDefaultValueInFunctionDefinition,
);
}
}
pub fn check_boolean_positional_value_in_function_call(checker: &mut Checker, args: &[Expr]) {
for arg in args {
add_if_boolean(
checker,
arg,
CheckKind::BooleanPositionalValueInFunctionCall,
);
}
}

View File

@@ -1,4 +1,3 @@
mod constants;
pub mod plugins;
pub mod settings;

View File

@@ -6,7 +6,7 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::new(
Default::default(),
Default::default(),
@@ -24,7 +24,7 @@ fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
},
)),
args: if let Some(msg) = msg {
vec![*msg.clone()]
vec![msg.clone()]
} else {
vec![]
},
@@ -36,14 +36,15 @@ fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
)
}
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Option<Box<Expr>>) {
/// B011
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) {
if let ExprKind::Constant {
value: Constant::Bool(false),
..
} = &test.node
{
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if checker.patch() {
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
if let Ok(content) = generator.generate() {

View File

@@ -54,7 +54,7 @@ fn duplicate_handler_exceptions<'a>(
),
Range::from_located(expr),
);
if checker.patch() {
if checker.patch(check.kind.code()) {
// TODO(charlie): If we have a single element, remove the tuple.
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&type_pattern(unique_elts), 0) {

View File

@@ -2,7 +2,7 @@ use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::{
collect_call_paths, compose_call_path, dealias_call_path, match_call_path,
collect_call_paths, compose_call_path, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::Range;
use crate::ast::visitor;
@@ -104,14 +104,7 @@ pub fn function_call_argument_default(checker: &mut Checker, arguments: &Argumen
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|s| {
let s = s.as_str();
if let Some(index) = s.rfind('.') {
(&s[..index], &s[index + 1..])
} else {
("", s)
}
})
.map(|target| to_module_and_member(target))
.collect();
let mut visitor = ArgumentDefaultVisitor {
checks: vec![],

View File

@@ -5,7 +5,7 @@ use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
fn attribute(value: &Expr, attr: &str) -> Expr {
@@ -20,6 +20,7 @@ fn attribute(value: &Expr, attr: &str) -> Expr {
)
}
/// B009
pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "getattr" {
@@ -32,7 +33,7 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
let mut check =
Check::new(CheckKind::GetAttrWithConstant, Range::from_located(expr));
if checker.patch() {
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&attribute(obj, value), 0) {
if let Ok(content) = generator.generate() {

View File

@@ -1,12 +1,12 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Operator};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
const MUTABLE_FUNCS: [(&str, &str); 7] = [
const MUTABLE_FUNCS: &[(&str, &str)] = &[
("", "dict"),
("", "list"),
("", "set"),
@@ -16,6 +16,47 @@ const MUTABLE_FUNCS: [(&str, &str); 7] = [
("collections", "deque"),
];
const IMMUTABLE_TYPES: &[(&str, &str)] = &[
("", "bool"),
("", "bytes"),
("", "complex"),
("", "float"),
("", "frozenset"),
("", "int"),
("", "object"),
("", "range"),
("", "str"),
("collections.abc", "Sized"),
("typing", "LiteralString"),
("typing", "Sized"),
];
const IMMUTABLE_GENERIC_TYPES: &[(&str, &str)] = &[
("", "tuple"),
("collections.abc", "ByteString"),
("collections.abc", "Collection"),
("collections.abc", "Container"),
("collections.abc", "Iterable"),
("collections.abc", "Mapping"),
("collections.abc", "Reversible"),
("collections.abc", "Sequence"),
("collections.abc", "Set"),
("typing", "AbstractSet"),
("typing", "ByteString"),
("typing", "Callable"),
("typing", "Collection"),
("typing", "Container"),
("typing", "FrozenSet"),
("typing", "Iterable"),
("typing", "Literal"),
("typing", "Mapping"),
("typing", "Never"),
("typing", "NoReturn"),
("typing", "Reversible"),
("typing", "Sequence"),
("typing", "Tuple"),
];
pub fn is_mutable_func(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
@@ -27,34 +68,106 @@ pub fn is_mutable_func(
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
fn is_mutable_expr(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match &expr.node {
ExprKind::List { .. }
| ExprKind::Dict { .. }
| ExprKind::Set { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => true,
ExprKind::Call { func, .. } => is_mutable_func(func, from_imports, import_aliases),
_ => false,
}
}
fn is_immutable_annotation(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match &expr.node {
ExprKind::Name { .. } | ExprKind::Attribute { .. } => {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
IMMUTABLE_TYPES
.iter()
.chain(IMMUTABLE_GENERIC_TYPES)
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
ExprKind::Subscript { value, slice, .. } => {
let call_path = dealias_call_path(collect_call_paths(value), import_aliases);
if IMMUTABLE_GENERIC_TYPES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
{
true
} else if match_call_path(&call_path, "typing", "Union", from_imports) {
if let ExprKind::Tuple { elts, .. } = &slice.node {
elts.iter()
.all(|elt| is_immutable_annotation(elt, from_imports, import_aliases))
} else {
false
}
} else if match_call_path(&call_path, "typing", "Optional", from_imports) {
is_immutable_annotation(slice, from_imports, import_aliases)
} else if match_call_path(&call_path, "typing", "Annotated", from_imports) {
if let ExprKind::Tuple { elts, .. } = &slice.node {
elts.first().map_or(false, |elt| {
is_immutable_annotation(elt, from_imports, import_aliases)
})
} else {
false
}
} else {
false
}
}
ExprKind::BinOp {
left,
op: Operator::BitOr,
right,
} => {
is_immutable_annotation(left, from_imports, import_aliases)
&& is_immutable_annotation(right, from_imports, import_aliases)
}
ExprKind::Constant {
value: Constant::None,
..
} => true,
_ => false,
}
}
/// B006
pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
for expr in arguments
.defaults
// Scan in reverse order to right-align zip()
for (arg, default) in arguments
.kwonlyargs
.iter()
.chain(arguments.kw_defaults.iter())
.rev()
.zip(arguments.kw_defaults.iter().rev())
.chain(
arguments
.args
.iter()
.rev()
.chain(arguments.posonlyargs.iter().rev())
.zip(arguments.defaults.iter().rev()),
)
{
match &expr.node {
ExprKind::List { .. }
| ExprKind::Dict { .. }
| ExprKind::Set { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
Range::from_located(expr),
));
}
ExprKind::Call { func, .. } => {
if is_mutable_func(func, &checker.from_imports, &checker.import_aliases) {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
Range::from_located(expr),
));
}
}
_ => {}
if is_mutable_expr(default, &checker.from_imports, &checker.import_aliases)
&& arg.node.annotation.as_ref().map_or(true, |expr| {
!is_immutable_annotation(expr, &checker.from_imports, &checker.import_aliases)
})
{
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
Range::from_located(default),
));
}
}
}

View File

@@ -1,26 +1,61 @@
use rustpython_ast::{Constant, Expr, ExprKind};
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
use crate::code_gen::SourceGenerator;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
fn assignment(obj: &Expr, name: &str, value: &Expr) -> Option<String> {
let stmt = Stmt::new(
Default::default(),
Default::default(),
StmtKind::Assign {
targets: vec![Expr::new(
Default::default(),
Default::default(),
ExprKind::Attribute {
value: Box::new(obj.clone()),
attr: name.to_string(),
ctx: ExprContext::Store,
},
)],
value: Box::new(value.clone()),
type_comment: None,
},
);
let mut generator = SourceGenerator::new();
match generator.unparse_stmt(&stmt) {
Ok(()) => generator.generate().ok(),
Err(_) => None,
}
}
/// B010
pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "setattr" {
if let [_, arg, _] = args {
if let [obj, name, value] = args {
if let ExprKind::Constant {
value: Constant::Str(value),
value: Constant::Str(name),
..
} = &arg.node
} = &name.node
{
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
checker.add_check(Check::new(
CheckKind::SetAttrWithConstant,
Range::from_located(expr),
));
if IDENTIFIER_REGEX.is_match(name) && !KWLIST.contains(&name.as_str()) {
let mut check =
Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(check.kind.code()) {
if let Some(content) = assignment(obj, name, value) {
check.amend(Fix::replacement(
content,
expr.location,
expr.end_location.unwrap(),
));
}
}
checker.add_check(check);
}
}
}

View File

@@ -65,7 +65,7 @@ pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body:
CheckKind::UnusedLoopControlVariable(name.to_string()),
Range::from_located(expr),
);
if checker.patch() {
if checker.patch(check.kind.code()) {
// Prefix the variable name with an underscore.
check.amend(Fix::replacement(
format!("_{name}"),

View File

@@ -16,7 +16,7 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
checker.settings.enabled.contains(&CheckCode::T203),
Range::from_located(expr),
) {
if checker.patch() {
if checker.patch(check.kind.code()) {
let context = checker.binding_context();
if matches!(
checker.parents[context.defined_by].node,

View File

@@ -1,4 +1,4 @@
//! Settings for the `flake-quotes` plugin.
//! Settings for the `flake8-quotes` plugin.
use serde::{Deserialize, Serialize};

View File

@@ -0,0 +1,26 @@
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_tidy_imports::settings::Strictness;
pub fn banned_relative_import(
stmt: &Stmt,
level: Option<&usize>,
strictness: &Strictness,
) -> Option<Check> {
if let Some(level) = level {
if level
> &match strictness {
Strictness::All => 0,
Strictness::Parents => 1,
}
{
return Some(Check::new(
CheckKind::BannedRelativeImport(strictness.clone()),
Range::from_located(stmt),
));
}
}
None
}

View File

@@ -0,0 +1,49 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::linter::test_path;
use crate::{flake8_tidy_imports, Settings};
#[test]
fn ban_parent_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/I252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::Parents,
},
..Settings::for_rules(vec![CheckCode::I252])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn ban_all_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/I252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::All,
},
..Settings::for_rules(vec![CheckCode::I252])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -0,0 +1,37 @@
//! Settings for the `flake8-tidy-imports` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Strictness {
Parents,
All,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub ban_relative_imports: Option<Strictness>,
}
#[derive(Debug, Hash)]
pub struct Settings {
pub ban_relative_imports: Strictness,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
ban_relative_imports: options.ban_relative_imports.unwrap_or(Strictness::Parents),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
ban_relative_imports: Strictness::Parents,
}
}
}

View File

@@ -0,0 +1,59 @@
---
source: src/flake8_tidy_imports/mod.rs
expression: checks
---
- kind:
BannedRelativeImport: all
location:
row: 1
column: 0
end_location:
row: 1
column: 21
fix: ~
- kind:
BannedRelativeImport: all
location:
row: 2
column: 0
end_location:
row: 2
column: 28
fix: ~
- kind:
BannedRelativeImport: all
location:
row: 4
column: 0
end_location:
row: 4
column: 21
fix: ~
- kind:
BannedRelativeImport: all
location:
row: 5
column: 0
end_location:
row: 5
column: 28
fix: ~
- kind:
BannedRelativeImport: all
location:
row: 7
column: 0
end_location:
row: 7
column: 27
fix: ~
- kind:
BannedRelativeImport: all
location:
row: 8
column: 0
end_location:
row: 8
column: 34
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/flake8_tidy_imports/mod.rs
expression: checks
---
- kind:
BannedRelativeImport: parents
location:
row: 4
column: 0
end_location:
row: 4
column: 21
fix: ~
- kind:
BannedRelativeImport: parents
location:
row: 5
column: 0
end_location:
row: 5
column: 28
fix: ~
- kind:
BannedRelativeImport: parents
location:
row: 7
column: 0
end_location:
row: 7
column: 27
fix: ~
- kind:
BannedRelativeImport: parents
location:
row: 8
column: 0
end_location:
row: 8
column: 34
fix: ~

View File

@@ -202,7 +202,7 @@ mod tests {
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -210,7 +210,7 @@ mod tests {
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -220,7 +220,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -228,7 +228,7 @@ mod tests {
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -238,7 +238,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -248,7 +248,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -258,7 +258,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, exclude.iter()));

View File

@@ -15,13 +15,13 @@ pub enum ImportType {
pub fn categorize(
module_base: &str,
level: &Option<usize>,
level: Option<&usize>,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> ImportType {
if level.map(|level| level > 0).unwrap_or(false) {
if level.map(|level| *level > 0).unwrap_or(false) {
ImportType::LocalFolder
} else if known_first_party.contains(module_base) {
ImportType::FirstParty

40
src/isort/comments.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::borrow::Cow;
use rustpython_ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::helpers;
use crate::ast::types::Range;
use crate::SourceCodeLocator;
#[derive(Debug)]
pub struct Comment<'a> {
pub value: Cow<'a, str>,
pub location: Location,
pub end_location: Location,
}
/// Collect all comments in an import block.
pub fn collect_comments<'a>(range: &Range, locator: &'a SourceCodeLocator) -> Vec<Comment<'a>> {
let contents = locator.slice_source_code_range(range);
lexer::make_tokenizer(&contents)
.flatten()
.filter_map(|(start, tok, end)| {
if matches!(tok, Tok::Comment) {
let start = helpers::to_absolute(&start, &range.location);
let end = helpers::to_absolute(&end, &range.location);
Some(Comment {
value: locator.slice_source_code_range(&Range {
location: start,
end_location: end,
}),
location: start,
end_location: end,
})
} else {
None
}
})
.collect()
}

182
src/isort/format.rs Normal file
View File

@@ -0,0 +1,182 @@
use crate::isort::types::{AliasData, CommentSet, ImportFromData, Importable};
// Hard-code four-space indentation for the imports themselves, to match Black.
const INDENT: &str = " ";
// Guess a capacity to use for string allocation.
const CAPACITY: usize = 200;
/// Add a plain import statement to the `RopeBuilder`.
pub fn format_import(alias: &AliasData, comments: &CommentSet, is_first: bool) -> String {
let mut output = String::with_capacity(CAPACITY);
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
if let Some(asname) = alias.asname {
output.push_str("import ");
output.push_str(alias.name);
output.push_str(" as ");
output.push_str(asname);
} else {
output.push_str("import ");
output.push_str(alias.name);
}
for comment in &comments.inline {
output.push_str(" ");
output.push_str(comment);
}
output.push('\n');
output
}
/// Add an import-from statement to the `RopeBuilder`.
pub fn format_import_from(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
line_length: &usize,
is_first: bool,
) -> String {
// We can only inline if: (1) none of the aliases have atop comments, and (3)
// only the last alias (if any) has inline comments.
if aliases
.iter()
.all(|(_, CommentSet { atop, .. })| atop.is_empty())
&& aliases
.iter()
.rev()
.skip(1)
.all(|(_, CommentSet { inline, .. })| inline.is_empty())
{
let (single_line, import_length) =
format_single_line(import_from, comments, aliases, is_first);
if import_length <= *line_length {
return single_line;
}
}
format_multi_line(import_from, comments, aliases, is_first)
}
/// Format an import-from statement in single-line format.
///
/// This method assumes that the output source code is syntactically valid.
fn format_single_line(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
is_first: bool,
) -> (String, usize) {
let mut output = String::with_capacity(CAPACITY);
let mut line_length = 0;
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
let module_name = import_from.module_name();
output.push_str("from ");
output.push_str(&module_name);
output.push_str(" import ");
line_length += 5 + module_name.len() + 8;
for (index, (AliasData { name, asname }, comments)) in aliases.iter().enumerate() {
if let Some(asname) = asname {
output.push_str(name);
output.push_str(" as ");
output.push_str(asname);
line_length += name.len() + 4 + asname.len();
} else {
output.push_str(name);
line_length += name.len();
}
if index < aliases.len() - 1 {
output.push_str(", ");
line_length += 2;
}
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_length += 2 + comment.len();
}
}
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_length += 2 + comment.len();
}
output.push('\n');
(output, line_length)
}
/// Format an import-from statement in multi-line format.
fn format_multi_line(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
is_first: bool,
) -> String {
let mut output = String::with_capacity(CAPACITY);
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
output.push_str("from ");
output.push_str(&import_from.module_name());
output.push_str(" import ");
output.push('(');
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
}
output.push('\n');
for (AliasData { name, asname }, comments) in aliases {
for comment in &comments.atop {
output.push_str(INDENT);
output.push_str(comment);
output.push('\n');
}
output.push_str(INDENT);
if let Some(asname) = asname {
output.push_str(name);
output.push_str(" as ");
output.push_str(asname);
} else {
output.push_str(name);
}
output.push(',');
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
}
output.push('\n');
}
output.push(')');
output.push('\n');
output
}

View File

@@ -1,64 +1,266 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use fnv::FnvHashSet;
use fnv::FnvHashMap;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
use crate::isort::categorize::{categorize, ImportType};
use crate::isort::comments::Comment;
use crate::isort::sorting::{member_key, module_key};
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable, OrderedImportBlock};
use crate::isort::types::{
AliasData, CommentSet, ImportBlock, ImportFromData, Importable, OrderedImportBlock,
};
mod categorize;
mod comments;
pub mod format;
pub mod plugins;
pub mod settings;
mod sorting;
pub mod track;
mod types;
// Hard-code four-space indentation for the imports themselves, to match Black.
const INDENT: &str = " ";
#[derive(Debug)]
pub struct AnnotatedAliasData<'a> {
pub name: &'a str,
pub asname: Option<&'a String>,
pub atop: Vec<Comment<'a>>,
pub inline: Vec<Comment<'a>>,
}
#[derive(Debug)]
pub enum AnnotatedImport<'a> {
Import {
names: Vec<AliasData<'a>>,
atop: Vec<Comment<'a>>,
inline: Vec<Comment<'a>>,
},
ImportFrom {
module: Option<&'a String>,
names: Vec<AnnotatedAliasData<'a>>,
level: Option<&'a usize>,
atop: Vec<Comment<'a>>,
inline: Vec<Comment<'a>>,
},
}
fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
let mut block: ImportBlock = Default::default();
fn annotate_imports<'a>(
imports: &'a [&'a Stmt],
comments: Vec<Comment<'a>>,
) -> Vec<AnnotatedImport<'a>> {
let mut annotated = vec![];
let mut comments_iter = comments.into_iter().peekable();
for import in imports {
match &import.node {
StmtKind::Import { names } => {
for name in names {
block.import.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
// Find comments above.
let mut atop = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
{
atop.push(comment);
}
// Find comments inline.
let mut inline = vec![];
while let Some(comment) = comments_iter.next_if(|comment| {
comment.end_location.row() == import.end_location.unwrap().row()
}) {
inline.push(comment);
}
annotated.push(AnnotatedImport::Import {
names: names
.iter()
.map(|alias| AliasData {
name: &alias.node.name,
asname: alias.node.asname.as_ref(),
})
.collect(),
atop,
inline,
});
}
StmtKind::ImportFrom {
module,
names,
level,
} => {
for name in names {
if name.node.asname.is_none() {
block
// Find comments above.
let mut atop = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
{
atop.push(comment);
}
// Find comments inline.
let mut inline = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() == import.location.row())
{
inline.push(comment);
}
// Capture names.
let mut aliases = vec![];
for alias in names {
// Find comments above.
let mut alias_atop = vec![];
while let Some(comment) = comments_iter
.next_if(|comment| comment.location.row() < alias.location.row())
{
alias_atop.push(comment);
}
// Find comments inline.
let mut alias_inline = vec![];
while let Some(comment) = comments_iter.next_if(|comment| {
comment.end_location.row() == alias.end_location.unwrap().row()
}) {
alias_inline.push(comment);
}
aliases.push(AnnotatedAliasData {
name: &alias.node.name,
asname: alias.node.asname.as_ref(),
atop: alias_atop,
inline: alias_inline,
})
}
annotated.push(AnnotatedImport::ImportFrom {
module: module.as_ref(),
names: aliases,
level: level.as_ref(),
atop,
inline,
});
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
annotated
}
fn normalize_imports(imports: Vec<AnnotatedImport>) -> ImportBlock {
let mut block: ImportBlock = Default::default();
for import in imports {
match import {
AnnotatedImport::Import {
names,
atop,
inline,
} => {
// Associate the comments with the first alias (best effort).
if let Some(name) = names.first() {
let entry = block
.import
.entry(AliasData {
name: name.name,
asname: name.asname,
})
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
}
// Create an entry for every alias.
for name in &names {
block
.import
.entry(AliasData {
name: name.name,
asname: name.asname,
})
.or_default();
}
}
AnnotatedImport::ImportFrom {
module,
names,
level,
atop,
inline,
} => {
// Associate the comments with the first alias (best effort).
if let Some(alias) = names.first() {
if alias.asname.is_none() {
let entry = &mut block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
.0;
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
} else {
block.import_from_as.insert((
ImportFromData { module, level },
AliasData {
name: &name.node.name,
asname: &name.node.asname,
},
));
let entry = &mut block
.import_from_as
.entry((
ImportFromData { module, level },
AliasData {
name: alias.name,
asname: alias.asname,
},
))
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
}
}
// Create an entry for every alias.
for alias in names {
if alias.asname.is_none() {
let entry = block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.1
.entry(AliasData {
name: alias.name,
asname: alias.asname,
})
.or_default();
for comment in alias.atop {
entry.atop.push(comment.value);
}
for comment in alias.inline {
entry.inline.push(comment.value);
}
} else {
let entry = block
.import_from_as
.entry((
ImportFromData { module, level },
AliasData {
name: alias.name,
asname: alias.asname,
},
))
.or_default();
entry
.atop
.extend(alias.atop.into_iter().map(|comment| comment.value));
for comment in alias.inline {
entry.inline.push(comment.value);
}
}
}
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
block
@@ -73,10 +275,10 @@ fn categorize_imports<'a>(
) -> BTreeMap<ImportType, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = Default::default();
// Categorize `StmtKind::Import`.
for alias in block.import {
for (alias, comments) in block.import {
let import_type = categorize(
&alias.module_base(),
&None,
None,
src,
known_first_party,
known_third_party,
@@ -86,7 +288,7 @@ fn categorize_imports<'a>(
.entry(import_type)
.or_default()
.import
.insert(alias);
.insert(alias, comments);
}
// Categorize `StmtKind::ImportFrom` (without re-export).
for (import_from, aliases) in block.import_from {
@@ -105,7 +307,7 @@ fn categorize_imports<'a>(
.insert(import_from, aliases);
}
// Categorize `StmtKind::ImportFrom` (with re-export).
for (import_from, alias) in block.import_from_as {
for ((import_from, alias), comments) in block.import_from_as {
let classification = categorize(
&import_from.module_base(),
import_from.level,
@@ -118,7 +320,7 @@ fn categorize_imports<'a>(
.entry(classification)
.or_default()
.import_from_as
.insert((import_from, alias));
.insert((import_from, alias), comments);
}
block_by_type
}
@@ -131,7 +333,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
.sorted_by_cached_key(|(alias, _)| module_key(alias.name, alias.asname)),
);
// Sort `StmtKind::ImportFrom`.
@@ -145,29 +347,47 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
block
.import_from_as
.into_iter()
.map(|(import_from, alias)| (import_from, FnvHashSet::from_iter([alias]))),
.map(|((import_from, alias), comments)| {
(
import_from,
(
CommentSet {
atop: comments.atop,
inline: Default::default(),
},
FnvHashMap::from_iter([(
alias,
CommentSet {
atop: Default::default(),
inline: comments.inline,
},
)]),
),
)
}),
)
.map(|(import_from, aliases)| {
.map(|(import_from, (comments, aliases))| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
import_from,
comments,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name, alias.asname))
.collect::<Vec<AliasData>>(),
.sorted_by_cached_key(|(alias, _)| member_key(alias.name, alias.asname))
.collect::<Vec<(AliasData, CommentSet)>>(),
)
})
.sorted_by_cached_key(|(import_from, aliases)| {
.sorted_by_cached_key(|(import_from, _, aliases)| {
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
// members.
(
import_from
.module
.as_ref()
.map(|module| module_key(module, &None)),
.map(|module| module_key(module, None)),
aliases
.first()
.map(|alias| member_key(alias.name, alias.asname)),
.map(|(alias, _)| member_key(alias.name, alias.asname)),
)
}),
);
@@ -176,15 +396,18 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
}
pub fn format_imports(
block: Vec<&Stmt>,
block: &[&Stmt],
comments: Vec<Comment>,
line_length: &usize,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> String {
let block = annotate_imports(block, comments);
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(&block);
let block = normalize_imports(block);
// Categorize by type (e.g., first-party vs. third-party).
let block_by_type = categorize_imports(
@@ -195,81 +418,38 @@ pub fn format_imports(
extra_standard_library,
);
// Generate replacement source code.
let mut output = RopeBuilder::new();
let mut first_block = true;
// Generate replacement source code.
let mut is_first_block = true;
for import_block in block_by_type.into_values() {
let import_block = sort_imports(import_block);
// Add a blank line between every section.
if !first_block {
if !is_first_block {
output.append("\n");
} else {
first_block = false;
is_first_block = false;
}
let mut is_first_statement = true;
// Format `StmtKind::Import` statements.
for AliasData { name, asname } in import_block.import.iter() {
if let Some(asname) = asname {
output.append(&format!("import {} as {}\n", name, asname));
} else {
output.append(&format!("import {}\n", name));
}
for (alias, comments) in import_block.import.iter() {
output.append(&format::format_import(alias, comments, is_first_statement));
is_first_statement = false;
}
// Format `StmtKind::ImportFrom` statements.
for (import_from, aliases) in import_block.import_from.iter() {
let prelude: String = format!("from {} import ", import_from.module_name());
let members: Vec<String> = aliases
.iter()
.map(|AliasData { name, asname }| {
if let Some(asname) = asname {
format!("{} as {}", name, asname)
} else {
name.to_string()
}
})
.collect();
// Can we fit the import on a single line?
let expected_len: usize =
// `from base import `
prelude.len()
// `member( as alias)?`
+ members.iter().map(|part| part.len()).sum::<usize>()
// `, `
+ 2 * (members.len() - 1);
if expected_len <= *line_length {
// `from base import `
output.append(&prelude);
// `member( as alias)?(, )?`
for (index, part) in members.into_iter().enumerate() {
if index > 0 {
output.append(", ");
}
output.append(&part);
}
// `\n`
output.append("\n");
} else {
// `from base import (\n`
output.append(&prelude);
output.append("(");
output.append("\n");
// ` member( as alias)?,\n`
for part in members {
output.append(INDENT);
output.append(&part);
output.append(",");
output.append("\n");
}
// `)\n`
output.append(")");
output.append("\n");
}
for (import_from, comments, aliases) in import_block.import_from.iter() {
output.append(&format::format_import_from(
import_from,
comments,
aliases,
line_length,
is_first_statement,
));
is_first_statement = false;
}
}
output.finish().to_string()
@@ -287,13 +467,17 @@ mod tests {
use crate::linter::test_path;
use crate::Settings;
#[test_case(Path::new("add_newline_before_comments.py"))]
#[test_case(Path::new("combine_import_froms.py"))]
#[test_case(Path::new("comments.py"))]
#[test_case(Path::new("deduplicate_imports.py"))]
#[test_case(Path::new("fit_line_length.py"))]
#[test_case(Path::new("fit_line_length_comment.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("order_by_type.py"))]
#[test_case(Path::new("preserve_comment_order.py"))]
#[test_case(Path::new("preserve_indentation.py"))]
#[test_case(Path::new("reorder_within_section.py"))]
#[test_case(Path::new("separate_first_party_imports.py"))]
@@ -303,6 +487,7 @@ mod tests {
#[test_case(Path::new("skip.py"))]
#[test_case(Path::new("sort_similar_imports.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
#[test_case(Path::new("type_comments.py"))]
fn isort(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let mut checks = test_path(

View File

@@ -1,11 +1,12 @@
use rustpython_ast::{Location, Stmt};
use textwrap::{dedent, indent};
use crate::ast::helpers::{match_leading_content, match_trailing_content};
use crate::ast::types::Range;
use crate::ast::whitespace::leading_space;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckKind;
use crate::docstrings::helpers::leading_space;
use crate::isort::format_imports;
use crate::isort::{comments, format_imports};
use crate::{Check, Settings, SourceCodeLocator};
fn extract_range(body: &[&Stmt]) -> Range {
@@ -27,26 +28,6 @@ fn extract_indentation(body: &[&Stmt], locator: &SourceCodeLocator) -> String {
leading_space(&existing)
}
fn match_leading_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
let location = body.first().unwrap().location;
let range = Range {
location: Location::new(location.row(), 0),
end_location: location,
};
let prefix = locator.slice_source_code_range(&range);
prefix.chars().any(|char| !char.is_whitespace())
}
fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
let end_location = body.last().unwrap().end_location.unwrap();
let range = Range {
location: end_location,
end_location: Location::new(end_location.row() + 1, 0),
};
let suffix = locator.slice_source_code_range(&range);
suffix.chars().any(|char| !char.is_whitespace())
}
/// I001
pub fn check_imports(
body: Vec<&Stmt>,
@@ -57,13 +38,23 @@ pub fn check_imports(
let range = extract_range(&body);
let indentation = extract_indentation(&body, locator);
// Extract comments. Take care to grab any inline comments from the last line.
let comments = comments::collect_comments(
&Range {
location: range.location,
end_location: Location::new(range.end_location.row() + 1, 0),
},
locator,
);
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(&body, locator);
let has_trailing_content = match_trailing_content(&body, locator);
let has_leading_content = match_leading_content(body.first().unwrap(), locator);
let has_trailing_content = match_trailing_content(body.last().unwrap(), locator);
// Generate the sorted import block.
let expected = format_imports(
body,
&body,
comments,
&(settings.line_length - indentation.len()),
&settings.src,
&settings.isort.known_first_party,
@@ -73,7 +64,7 @@ pub fn check_imports(
if has_leading_content || has_trailing_content {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix.patch() {
if autofix.patch() && settings.fixable.contains(check.kind.code()) {
let mut content = String::new();
if has_leading_content {
content.push('\n');
@@ -101,7 +92,7 @@ pub fn check_imports(
let actual = dedent(&locator.slice_source_code_range(&range));
if actual != expected {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix.patch() {
if autofix.patch() && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::replacement(
indent(&expected, &indentation),
range.location,

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 8
column: 0
fix:
patch:
content: "import os\n\n# This is a comment in the same section, so we need to add one newline.\nimport sys\n\nimport numpy as np\n\n# This is a comment, but it starts a new section, so we don't need to add a newline\n# before it.\nimport leading_prefix\n"
location:
row: 1
column: 0
end_location:
row: 8
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 3
column: 0
end_location:
row: 26
column: 0
fix:
patch:
content: "import B # Comment 4\n\n# Comment 3a\n# Comment 3b\nimport C\nimport D\n\n# Comment 5\n# Comment 6\nfrom A import (\n a, # Comment 7 # Comment 9\n b, # Comment 10\n c, # Comment 8 # Comment 11\n)\n"
location:
row: 3
column: 0
end_location:
row: 26
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import a\n\n# Don't take this comment into account when determining whether the next import can fit on one line.\nfrom b import c\nfrom d import ( # Do take this comment into account when determining whether the next import can fit on one line.\n e,\n)\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 12
column: 0
fix:
patch:
content: "import abc\nimport io\n\n# Old MacDonald had a farm,\n# EIEIO\n# And on his farm he had a cow,\n# EIEIO\n# With a moo-moo here and a moo-moo there\n# Here a moo, there a moo, everywhere moo-moo\n# Old MacDonald had a farm,\n# EIEIO\nfrom errno import EIO\n"
location:
row: 1
column: 0
end_location:
row: 12
column: 0
applied: false

View File

@@ -0,0 +1,6 @@
---
source: src/isort/mod.rs
expression: checks
---
[]

View File

@@ -10,15 +10,15 @@ pub enum Prefix {
pub fn module_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (String, &'a str, &'a Option<String>) {
asname: Option<&'a String>,
) -> (String, &'a str, Option<&'a String>) {
(name.to_lowercase(), name, asname)
}
pub fn member_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (Prefix, String, &'a Option<String>) {
asname: Option<&'a String>,
) -> (Prefix, String, Option<&'a String>) {
(
if name.len() > 1 && string::is_upper(name) {
// Ex) `CONSTANT`

View File

@@ -1,15 +1,25 @@
use fnv::{FnvHashMap, FnvHashSet};
use std::borrow::Cow;
use fnv::FnvHashMap;
use crate::ast;
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct ImportFromData<'a> {
pub module: &'a Option<String>,
pub level: &'a Option<usize>,
pub module: Option<&'a String>,
pub level: Option<&'a usize>,
}
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct AliasData<'a> {
pub name: &'a str,
pub asname: &'a Option<String>,
pub asname: Option<&'a String>,
}
#[derive(Debug, Default)]
pub struct CommentSet<'a> {
pub atop: Vec<Cow<'a, str>>,
pub inline: Vec<Cow<'a, str>>,
}
pub trait Importable {
@@ -29,16 +39,7 @@ impl Importable for AliasData<'_> {
impl Importable for ImportFromData<'_> {
fn module_name(&self) -> String {
let mut module_name = String::new();
if let Some(level) = self.level {
if level > &0 {
module_name.push_str(&".".repeat(*level));
}
}
if let Some(module) = self.module {
module_name.push_str(module);
}
module_name
ast::helpers::format_import_from(self.level, self.module)
}
fn module_base(&self) -> String {
@@ -50,17 +51,24 @@ impl Importable for ImportFromData<'_> {
pub struct ImportBlock<'a> {
// Set of (name, asname), used to track regular imports.
// Ex) `import module`
pub import: FnvHashSet<AliasData<'a>>,
pub import: FnvHashMap<AliasData<'a>, CommentSet<'a>>,
// Map from (module, level) to `AliasData`, used to track 'from' imports.
// Ex) `from module import member`
pub import_from: FnvHashMap<ImportFromData<'a>, FnvHashSet<AliasData<'a>>>,
pub import_from:
FnvHashMap<ImportFromData<'a>, (CommentSet<'a>, FnvHashMap<AliasData<'a>, CommentSet<'a>>)>,
// Set of (module, level, name, asname), used to track re-exported 'from' imports.
// Ex) `from module import member as member`
pub import_from_as: FnvHashSet<(ImportFromData<'a>, AliasData<'a>)>,
pub import_from_as: FnvHashMap<(ImportFromData<'a>, AliasData<'a>), CommentSet<'a>>,
}
type AliasDataWithComments<'a> = (AliasData<'a>, CommentSet<'a>);
#[derive(Debug, Default)]
pub struct OrderedImportBlock<'a> {
pub import: Vec<AliasData<'a>>,
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
pub import: Vec<AliasDataWithComments<'a>>,
pub import_from: Vec<(
ImportFromData<'a>,
CommentSet<'a>,
Vec<AliasDataWithComments<'a>>,
)>,
}

View File

@@ -30,16 +30,20 @@ mod docstrings;
mod flake8_2020;
pub mod flake8_annotations;
pub mod flake8_bandit;
mod flake8_blind_except;
pub mod flake8_boolean_trap;
pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_print;
pub mod flake8_quotes;
pub mod flake8_tidy_imports;
pub mod fs;
mod isort;
mod lex;
pub mod linter;
pub mod logging;
pub mod mccabe;
pub mod message;
mod noqa;
pub mod pep8_naming;
@@ -64,14 +68,16 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
let pyproject = pyproject::find_pyproject_toml(project_root.as_ref());
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings =
Settings::from_configuration(Configuration::from_pyproject(&pyproject, &project_root)?);
let settings = Settings::from_configuration(Configuration::from_pyproject(
pyproject.as_ref(),
project_root.as_ref(),
)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -22,7 +22,7 @@ use crate::check_tokens::check_tokens;
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::code_gen::SourceGenerator;
use crate::directives::Directives;
use crate::message::Message;
use crate::message::{Message, Source};
use crate::noqa::add_noqa;
use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
@@ -176,11 +176,18 @@ pub fn lint_stdin(
// Convert to messages.
Ok(checks
.into_iter()
.map(|check| Message::from_check(path.to_string_lossy().to_string(), check))
.map(|check| {
let filename = path.to_string_lossy().to_string();
let source = if settings.show_source {
Some(Source::from_check(&check, &locator))
} else {
None
};
Message::from_check(check, filename, source)
})
.collect())
}
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
pub fn lint_path(
path: &Path,
settings: &Settings,
@@ -190,7 +197,6 @@ pub fn lint_path(
let metadata = path.metadata()?;
// Check the cache.
#[cfg(not(target_family = "wasm"))]
if let Some(messages) = cache::get(path, &metadata, settings, autofix, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
@@ -233,9 +239,17 @@ pub fn lint_path(
// Convert to messages.
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message::from_check(path.to_string_lossy().to_string(), check))
.map(|check| {
let filename = path.to_string_lossy().to_string();
let source = if settings.show_source {
Some(Source::from_check(&check, &locator))
} else {
None
};
Message::from_check(check, filename, source)
})
.collect();
#[cfg(not(target_family = "wasm"))]
cache::set(path, &metadata, settings, autofix, &messages, mode);
Ok(messages)
@@ -351,6 +365,7 @@ mod tests {
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::BLE001, Path::new("BLE.py"); "BLE001")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
#[test_case(CheckCode::C402, Path::new("C402.py"); "C402")]
@@ -487,7 +502,6 @@ mod tests {
#[test_case(CheckCode::T201, Path::new("T201.py"); "T201")]
#[test_case(CheckCode::T203, Path::new("T203.py"); "T203")]
#[test_case(CheckCode::U001, Path::new("U001.py"); "U001")]
#[test_case(CheckCode::U002, Path::new("U002.py"); "U002")]
#[test_case(CheckCode::U003, Path::new("U003.py"); "U003")]
#[test_case(CheckCode::U004, Path::new("U004.py"); "U004")]
#[test_case(CheckCode::U005, Path::new("U005.py"); "U005")]
@@ -502,6 +516,8 @@ mod tests {
#[test_case(CheckCode::U011, Path::new("U011_0.py"); "U011_0")]
#[test_case(CheckCode::U011, Path::new("U011_1.py"); "U011_1")]
#[test_case(CheckCode::U012, Path::new("U012.py"); "U012")]
#[test_case(CheckCode::U013, Path::new("U013.py"); "U013")]
#[test_case(CheckCode::U014, Path::new("U014.py"); "U014")]
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]
@@ -520,6 +536,9 @@ mod tests {
#[test_case(CheckCode::YTT301, Path::new("YTT301.py"); "YTT301")]
#[test_case(CheckCode::YTT302, Path::new("YTT302.py"); "YTT302")]
#[test_case(CheckCode::YTT303, Path::new("YTT303.py"); "YTT303")]
#[test_case(CheckCode::FBT001, Path::new("FBT.py"); "FBT001")]
#[test_case(CheckCode::FBT002, Path::new("FBT.py"); "FBT002")]
#[test_case(CheckCode::FBT003, Path::new("FBT.py"); "FBT003")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -4,10 +4,9 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
#[cfg(not(target_family = "wasm"))]
use ::ruff::cache;
use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, warn_on, Cli, Warnable};
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, Cli};
use ::ruff::fs::iter_python_files;
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin};
use ::ruff::logging::{set_up_logging, LogLevel};
@@ -117,6 +116,7 @@ fn run_once(
location: Default::default(),
end_location: Default::default(),
filename: path.to_string_lossy().to_string(),
source: None,
}]
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
@@ -208,7 +208,7 @@ fn inner_main() -> Result<ExitCode> {
};
let pyproject = cli
.config
.or_else(|| pyproject::find_pyproject_toml(&project_root));
.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
@@ -218,15 +218,16 @@ fn inner_main() -> Result<ExitCode> {
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect();
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect();
let mut configuration = Configuration::from_pyproject(&pyproject, &project_root)?;
let mut configuration =
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
if !exclude.is_empty() {
configuration.exclude = exclude;
}
@@ -235,28 +236,12 @@ fn inner_main() -> Result<ExitCode> {
}
if !cli.per_file_ignores.is_empty() {
configuration.per_file_ignores =
collect_per_file_ignores(cli.per_file_ignores, &project_root);
collect_per_file_ignores(cli.per_file_ignores, project_root.as_ref());
}
if !cli.select.is_empty() {
warn_on(
Warnable::Select,
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&configuration,
&pyproject,
);
configuration.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
Warnable::ExtendSelect,
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&configuration,
&pyproject,
);
configuration.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
@@ -265,9 +250,18 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
configuration.extend_ignore = cli.extend_ignore;
}
if !cli.fixable.is_empty() {
configuration.fixable = cli.fixable;
}
if !cli.unfixable.is_empty() {
configuration.unfixable = cli.unfixable;
}
if let Some(line_length) = cli.line_length {
configuration.line_length = line_length;
}
if let Some(max_complexity) = cli.max_complexity {
configuration.mccabe.max_complexity = max_complexity;
}
if let Some(target_version) = cli.target_version {
configuration.target_version = target_version;
}
@@ -277,6 +271,9 @@ fn inner_main() -> Result<ExitCode> {
if let Some(fix) = fix {
configuration.fix = fix;
}
if cli.show_source {
configuration.show_source = true;
}
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
@@ -288,7 +285,7 @@ fn inner_main() -> Result<ExitCode> {
}
// Extract settings for internal use.
let autofix = configuration.fix;
let fix_enabled: bool = configuration.fix;
let settings = Settings::from_configuration(configuration);
if cli.show_files {
@@ -296,12 +293,16 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::SUCCESS);
}
#[cfg(not(target_family = "wasm"))]
cache::init()?;
// Initialize the cache.
let mut cache_enabled: bool = !cli.no_cache;
if cache_enabled && cache::init().is_err() {
eprintln!("Unable to initialize cache; disabling...");
cache_enabled = false;
}
let printer = Printer::new(&cli.format, &log_level);
if cli.watch {
if autofix {
if fix_enabled {
eprintln!("Warning: --fix is not enabled in watch mode.");
}
@@ -321,7 +322,7 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
let messages = run_once(&cli.files, &settings, cache_enabled, false)?;
printer.write_continuously(&messages)?;
// Configure the file watcher.
@@ -339,7 +340,7 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?;
printer.write_to_user("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
let messages = run_once(&cli.files, &settings, cache_enabled, false)?;
printer.write_continuously(&messages)?;
}
}
@@ -364,15 +365,15 @@ fn inner_main() -> Result<ExitCode> {
let messages = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
run_once_stdin(&settings, path, autofix)?
run_once_stdin(&settings, path, fix_enabled)?
} else {
run_once(&cli.files, &settings, !cli.no_cache, autofix)?
run_once(&cli.files, &settings, cache_enabled, fix_enabled)?
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && autofix) {
if !(is_stdin && fix_enabled) {
printer.write_once(&messages)?;
}

73
src/mccabe/checks.rs Normal file
View File

@@ -0,0 +1,73 @@
use rustpython_ast::{ExcepthandlerKind, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
fn get_complexity_number(stmts: &[Stmt]) -> usize {
let mut complexity = 0;
for stmt in stmts {
match &stmt.node {
StmtKind::If { body, orelse, .. } => {
complexity += 1;
complexity += get_complexity_number(body);
complexity += get_complexity_number(orelse);
}
StmtKind::For { body, orelse, .. } | StmtKind::AsyncFor { body, orelse, .. } => {
complexity += 1;
complexity += get_complexity_number(body);
complexity += get_complexity_number(orelse);
}
StmtKind::While { test, body, orelse } => {
complexity += 1;
complexity += get_complexity_number(body);
complexity += get_complexity_number(orelse);
if let ExprKind::BoolOp { .. } = &test.node {
complexity += 1;
}
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
complexity += 1;
complexity += get_complexity_number(body);
complexity += get_complexity_number(orelse);
complexity += get_complexity_number(finalbody);
for handler in handlers {
complexity += 1;
let ExcepthandlerKind::ExceptHandler { body, .. } = &handler.node;
complexity += get_complexity_number(body);
}
}
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
complexity += 1;
complexity += get_complexity_number(body);
}
StmtKind::ClassDef { body, .. } => {
complexity += 1;
complexity += get_complexity_number(body);
}
_ => {}
}
}
complexity
}
pub fn function_is_too_complex(
stmt: &Stmt,
name: &str,
body: &[Stmt],
max_complexity: usize,
) -> Option<Check> {
let complexity = get_complexity_number(body) + 1;
if complexity > max_complexity {
Some(Check::new(
CheckKind::FunctionIsTooComplex(name.to_string(), complexity),
Range::from_located(stmt),
))
} else {
None
}
}

33
src/mccabe/mod.rs Normal file
View File

@@ -0,0 +1,33 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{mccabe, Settings};
#[test_case(0)]
#[test_case(3)]
#[test_case(10)]
fn max_complexity_zero(max_complexity: usize) -> Result<()> {
let snapshot = format!("max_complexity_{}", max_complexity);
let mut checks = test_path(
Path::new("./resources/test/fixtures/C901.py"),
&Settings {
mccabe: mccabe::settings::Settings { max_complexity },
..Settings::for_rules(vec![CheckCode::C901])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

28
src/mccabe/settings.rs Normal file
View File

@@ -0,0 +1,28 @@
//! Settings for the `mccabe` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub max_complexity: Option<usize>,
}
#[derive(Debug, Hash)]
pub struct Settings {
pub max_complexity: usize,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
max_complexity: options.max_complexity.unwrap_or_default(),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self { max_complexity: 10 }
}
}

View File

@@ -0,0 +1,170 @@
---
source: src/mccabe/mod.rs
expression: checks
---
- kind:
FunctionIsTooComplex:
- trivial
- 1
location:
row: 2
column: 0
end_location:
row: 7
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- expr_as_statement
- 1
location:
row: 7
column: 0
end_location:
row: 12
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- sequential
- 1
location:
row: 12
column: 0
end_location:
row: 19
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- if_elif_else_dead_path
- 3
location:
row: 19
column: 0
end_location:
row: 29
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- nested_ifs
- 3
location:
row: 29
column: 0
end_location:
row: 40
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- for_loop
- 2
location:
row: 40
column: 0
end_location:
row: 46
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- for_else
- 2
location:
row: 46
column: 0
end_location:
row: 54
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- recursive
- 2
location:
row: 54
column: 0
end_location:
row: 62
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- nested_functions
- 3
location:
row: 62
column: 0
end_location:
row: 73
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- a
- 2
location:
row: 63
column: 4
end_location:
row: 69
column: 4
fix: ~
- kind:
FunctionIsTooComplex:
- b
- 1
location:
row: 64
column: 8
end_location:
row: 67
column: 8
fix: ~
- kind:
FunctionIsTooComplex:
- try_else
- 4
location:
row: 73
column: 0
end_location:
row: 85
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- nested_try_finally
- 3
location:
row: 85
column: 0
end_location:
row: 96
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- foobar
- 3
location:
row: 96
column: 0
end_location:
row: 107
column: 0
fix: ~
- kind:
FunctionIsTooComplex:
- annotated_assign
- 1
location:
row: 107
column: 0
end_location:
row: 109
column: 0
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/mccabe/mod.rs
expression: checks
---
[]

View File

@@ -0,0 +1,16 @@
---
source: src/mccabe/mod.rs
expression: checks
---
- kind:
FunctionIsTooComplex:
- try_else
- 4
location:
row: 73
column: 0
end_location:
row: 85
column: 0
fix: ~

View File

@@ -2,12 +2,16 @@ use std::cmp::Ordering;
use std::fmt;
use std::path::Path;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::fs::relativize_path;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
@@ -16,16 +20,18 @@ pub struct Message {
pub location: Location,
pub end_location: Location,
pub filename: String,
pub source: Option<Source>,
}
impl Message {
pub fn from_check(filename: String, check: Check) -> Self {
pub fn from_check(check: Check, filename: String, source: Option<Source>) -> Self {
Self {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: Location::new(check.location.row(), check.location.column() + 1),
end_location: Location::new(check.end_location.row(), check.end_location.column() + 1),
filename,
source,
}
}
}
@@ -48,8 +54,7 @@ impl PartialOrd for Message {
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
let label = format!(
"{}{}{}{}{}{} {} {}",
relativize_path(Path::new(&self.filename)).white().bold(),
":".cyan(),
@@ -58,7 +63,71 @@ impl fmt::Display for Message {
self.location.column(),
":".cyan(),
self.kind.code().as_ref().red().bold(),
self.kind.body()
)
self.kind.body(),
);
match &self.source {
None => write!(f, "{}", label),
Some(source) => {
let snippet = Snippet {
title: Some(Annotation {
label: Some(&label),
annotation_type: AnnotationType::Error,
// The ID (error number) is already encoded in the `label`.
id: None,
}),
footer: vec![],
slices: vec![Slice {
source: &source.contents,
line_start: self.location.row(),
annotations: vec![SourceAnnotation {
label: self.kind.code().as_ref(),
annotation_type: AnnotationType::Error,
range: source.range,
}],
// The origin (file name, line number, and column number) is already encoded
// in the `label`.
origin: None,
fold: false,
}],
opt: FormatOptions {
color: true,
..Default::default()
},
};
// `split_once(' ')` strips "error: " from `message`.
let message = DisplayList::from(snippet).to_string();
let (_, message) = message.split_once(' ').unwrap();
write!(f, "{}", message)
}
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Source {
pub contents: String,
pub range: (usize, usize),
}
impl Source {
pub fn from_check(check: &Check, locator: &SourceCodeLocator) -> Self {
let source = locator.slice_source_code_range(&Range {
location: Location::new(check.location.row(), 0),
end_location: Location::new(check.end_location.row() + 1, 0),
});
let num_chars_in_range = locator
.slice_source_code_range(&Range {
location: check.location,
end_location: check.end_location,
})
.chars()
.count();
Source {
contents: source.to_string(),
range: (
check.location.column(),
check.location.column() + num_chars_in_range,
),
}
}
}

View File

@@ -10,32 +10,40 @@ use regex::Regex;
use crate::checks::{Check, CheckCode};
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<noqa>\s*(?i:# noqa)(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
.expect("Invalid regex")
Regex::new(
r"(?P<spaces>\s*)(?P<noqa>(?i:# noqa)(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)",
)
.expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
#[derive(Debug)]
pub enum Directive<'a> {
None,
All(usize, usize),
Codes(usize, usize, Vec<&'a str>),
All(usize, usize, usize),
Codes(usize, usize, usize, Vec<&'a str>),
}
pub fn extract_noqa_directive(line: &str) -> Directive {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => Directive::Codes(
noqa.start(),
noqa.end(),
SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
.collect(),
),
None => Directive::All(noqa.start(), noqa.end()),
Some(caps) => match caps.name("spaces") {
Some(spaces) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => Directive::Codes(
spaces.as_str().chars().count(),
noqa.start(),
noqa.end(),
SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
.collect(),
),
None => {
Directive::All(spaces.as_str().chars().count(), noqa.start(), noqa.end())
}
},
None => Directive::None,
},
None => Directive::None,
},
@@ -72,10 +80,7 @@ fn add_noqa_inner(
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
if !codes.is_empty() {
let matches = matches_by_line.entry(noqa_lineno).or_default();
@@ -95,12 +100,14 @@ fn add_noqa_inner(
match extract_noqa_directive(line) {
Directive::None => {
output.push_str(line);
output.push_str(" # noqa: ");
}
Directive::All(_, start, _) | Directive::Codes(_, start, ..) => {
output.push_str(&line[..start]);
output.push_str("# noqa: ");
}
Directive::All(start, _) => output.push_str(&line[..start]),
Directive::Codes(start, ..) => output.push_str(&line[..start]),
};
let codes: Vec<&str> = codes.iter().map(|code| code.as_ref()).collect();
output.push_str(" # noqa: ");
output.push_str(&codes.join(", "));
output.push('\n');
count += 1;

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