Compare commits
66 Commits
v0.1.11
...
simplify-S
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
827747ddcd | ||
|
|
e871037f7c | ||
|
|
957aba0be9 | ||
|
|
76e40b2ed3 | ||
|
|
f192c72596 | ||
|
|
25bafd2d66 | ||
|
|
7fc51d29c5 | ||
|
|
2d362e9366 | ||
|
|
501bc1c270 | ||
|
|
a4c6246ad7 | ||
|
|
14d3fe6bfa | ||
|
|
7e1309b854 | ||
|
|
4a3bb67b5f | ||
|
|
0ae19efe74 | ||
|
|
79f4abbb8d | ||
|
|
58fcd96ac1 | ||
|
|
ac02d3aedd | ||
|
|
6be73322da | ||
|
|
ad1ca72a35 | ||
|
|
381811b4a6 | ||
|
|
20af5a774f | ||
|
|
86b1ae9383 | ||
|
|
84ab21f073 | ||
|
|
985f1d10f6 | ||
|
|
f419af494f | ||
|
|
94968fedd5 | ||
|
|
ba71772d93 | ||
|
|
b1a5df8694 | ||
|
|
0c84782060 | ||
|
|
d9fc9702cf | ||
|
|
c58d1aa87d | ||
|
|
1d1824787b | ||
|
|
04afdf177b | ||
|
|
d5a439cbd3 | ||
|
|
63953431a6 | ||
|
|
f6841757eb | ||
|
|
1666c7a5cb | ||
|
|
e80b3db10d | ||
|
|
701697c37e | ||
|
|
c2c9997682 | ||
|
|
cee09765ef | ||
|
|
cde4a7d7bf | ||
|
|
62eca330a8 | ||
|
|
59078c5403 | ||
|
|
6bf6521197 | ||
|
|
c11f65381f | ||
|
|
6dfc1ccd6f | ||
|
|
60ba7a7c0d | ||
|
|
4b8b3a1ced | ||
|
|
967b2dcaf4 | ||
|
|
aaa00976ae | ||
|
|
328262bfac | ||
|
|
f0d43dafcf | ||
|
|
9a14f403c8 | ||
|
|
1293383cdc | ||
|
|
3b323a09cb | ||
|
|
5c93a524f1 | ||
|
|
e3ad163785 | ||
|
|
0e202718fd | ||
|
|
7b6baff734 | ||
|
|
1ffc738c84 | ||
|
|
dc5094d42a | ||
|
|
fd36754beb | ||
|
|
154d3b9f4b | ||
|
|
6c0734680e | ||
|
|
eac67a9464 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -168,7 +168,7 @@ jobs:
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: false # ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo fuzz"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
- name: "Install cargo-fuzz"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz@0.11
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- run: cargo fuzz build -s none
|
||||
|
||||
scripts:
|
||||
|
||||
@@ -370,7 +370,7 @@ See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/pyt
|
||||
We have several ways of benchmarking and profiling Ruff:
|
||||
|
||||
- Our main performance benchmark comparing Ruff with other tools on the CPython codebase
|
||||
- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests.
|
||||
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests.
|
||||
- Profiling the linter on either the microbenchmarks or entire projects
|
||||
|
||||
### CPython Benchmark
|
||||
|
||||
104
Cargo.lock
generated
104
Cargo.lock
generated
@@ -123,9 +123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.76"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -312,9 +312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.12"
|
||||
version = "4.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d"
|
||||
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -382,7 +382,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -600,7 +600,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -611,7 +611,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -748,23 +748,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.3"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1107,7 +1096,7 @@ dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1250,9 +1239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.149"
|
||||
version = "0.2.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
|
||||
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1297,9 +1286,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.10"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
||||
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -1696,7 +1685,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1781,9 +1770,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.73"
|
||||
version = "1.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1"
|
||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1827,9 +1816,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1996,7 +1985,7 @@ dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2248,7 +2237,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2259,6 +2248,7 @@ dependencies = [
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -2538,15 +2528,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.21"
|
||||
version = "0.38.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2652,9 +2642,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.193"
|
||||
version = "1.0.195"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2672,13 +2662,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.193"
|
||||
version = "1.0.195"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2740,7 +2730,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2844,7 +2834,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2860,9 +2850,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.40"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2871,15 +2861,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.1"
|
||||
version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2940,7 +2930,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2952,7 +2942,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -2973,7 +2963,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3110,7 +3100,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3328,7 +3318,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3422,7 +3412,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3456,7 +3446,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3489,7 +3479,7 @@ checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3816,5 +3806,5 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.40",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
@@ -14,14 +14,14 @@ license = "MIT"
|
||||
[workspace.dependencies]
|
||||
aho-corasick = { version = "1.1.2" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
anyhow = { version = "1.0.76" }
|
||||
anyhow = { version = "1.0.79" }
|
||||
argfile = { version = "0.1.6" }
|
||||
assert_cmd = { version = "2.0.8" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.4.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.4.12", features = ["derive"] }
|
||||
clap = { version = "4.4.13", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "2.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
|
||||
@@ -76,7 +76,7 @@ rustc-hash = { version = "1.1.0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version ="4.1.0"}
|
||||
semver = { version = "1.0.20" }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde-wasm-bindgen = { version = "0.6.3" }
|
||||
serde_json = { version = "1.0.109" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
@@ -89,7 +89,7 @@ static_assertions = "1.1.0"
|
||||
strum = { version = "0.25.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.25.3" }
|
||||
syn = { version = "2.0.40" }
|
||||
tempfile = { version ="3.8.1"}
|
||||
tempfile = { version ="3.9.0"}
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "1.0.51" }
|
||||
tikv-jemallocator = { version ="0.5.0"}
|
||||
|
||||
@@ -386,6 +386,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- Benchling ([Refac](https://github.com/benchling/refac))
|
||||
- [Bokeh](https://github.com/bokeh/bokeh)
|
||||
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
|
||||
- CERN ([Indico](https://getindico.io/))
|
||||
- [DVC](https://github.com/iterative/dvc)
|
||||
- [Dagger](https://github.com/dagger/dagger)
|
||||
- [Dagster](https://github.com/dagster-io/dagster)
|
||||
@@ -417,6 +418,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [NoneBot](https://github.com/nonebot/nonebot2)
|
||||
- [NumPyro](https://github.com/pyro-ppl/numpyro)
|
||||
- [ONNX](https://github.com/onnx/onnx)
|
||||
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
@@ -428,6 +430,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PostHog](https://github.com/PostHog/posthog)
|
||||
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
|
||||
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
|
||||
- [PyMC](https://github.com/pymc-devs/pymc/)
|
||||
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
|
||||
@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
|
||||
&case,
|
||||
|b, case| {
|
||||
// Tokenize the source.
|
||||
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
|
||||
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
|
||||
|
||||
// Parse the source.
|
||||
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();
|
||||
|
||||
@@ -25,10 +25,9 @@ use ruff_notebook::NotebookIndex;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
|
||||
use ruff_workspace::resolver::Resolver;
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::cache;
|
||||
use crate::diagnostics::Diagnostics;
|
||||
|
||||
/// [`Path`] that is relative to the package root in [`PackageCache`].
|
||||
@@ -86,6 +85,7 @@ pub(crate) struct Cache {
|
||||
changes: Mutex<Vec<Change>>,
|
||||
/// The "current" timestamp used as cache for the updates of
|
||||
/// [`FileCache::last_seen`]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
last_seen_cache: u64,
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ pub(super) struct CacheMessage {
|
||||
pub(crate) trait PackageCaches {
|
||||
fn get(&self, package_root: &Path) -> Option<&Cache>;
|
||||
|
||||
fn persist(self) -> anyhow::Result<()>;
|
||||
fn persist(self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<T> PackageCaches for Option<T>
|
||||
@@ -468,27 +468,17 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
|
||||
|
||||
impl<'a> PackageCacheMap<'a> {
|
||||
pub(crate) fn init(
|
||||
pyproject_config: &PyprojectConfig,
|
||||
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
|
||||
resolver: &Resolver,
|
||||
) -> Self {
|
||||
fn init_cache(path: &Path) {
|
||||
if let Err(e) = cache::init(path) {
|
||||
if let Err(e) = init(path) {
|
||||
error!("Failed to initialize cache at {}: {e:?}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => {
|
||||
init_cache(&pyproject_config.settings.cache_dir);
|
||||
}
|
||||
PyprojectDiscoveryStrategy::Hierarchical => {
|
||||
for settings in
|
||||
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
|
||||
{
|
||||
init_cache(&settings.cache_dir);
|
||||
}
|
||||
}
|
||||
for settings in resolver.settings() {
|
||||
init_cache(&settings.cache_dir);
|
||||
}
|
||||
|
||||
Self(
|
||||
@@ -498,7 +488,7 @@ impl<'a> PackageCacheMap<'a> {
|
||||
.unique()
|
||||
.par_bridge()
|
||||
.map(|cache_root| {
|
||||
let settings = resolver.resolve(cache_root, pyproject_config);
|
||||
let settings = resolver.resolve(cache_root);
|
||||
let cache = Cache::open(cache_root.to_path_buf(), settings);
|
||||
(cache_root, cache)
|
||||
})
|
||||
|
||||
@@ -38,7 +38,6 @@ pub(crate) fn add_noqa(
|
||||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
pyproject_config,
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
@@ -57,7 +56,7 @@ pub(crate) fn add_noqa(
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(|package| *package);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
let settings = resolver.resolve(path);
|
||||
let source_kind = match SourceKind::from_path(path, source_type) {
|
||||
Ok(Some(source_kind)) => source_kind,
|
||||
Ok(None) => return None,
|
||||
|
||||
@@ -57,16 +57,11 @@ pub(crate) fn check(
|
||||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
pyproject_config,
|
||||
);
|
||||
|
||||
// Load the caches.
|
||||
let caches = if bool::from(cache) {
|
||||
Some(PackageCacheMap::init(
|
||||
pyproject_config,
|
||||
&package_roots,
|
||||
&resolver,
|
||||
))
|
||||
Some(PackageCacheMap::init(&package_roots, &resolver))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -81,7 +76,7 @@ pub(crate) fn check(
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(|package| *package);
|
||||
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
let settings = resolver.resolve(path);
|
||||
|
||||
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
|
||||
&& match_exclusion(
|
||||
@@ -128,7 +123,7 @@ pub(crate) fn check(
|
||||
|
||||
Some(result.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = &path {
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
let settings = resolver.resolve(path);
|
||||
if settings.linter.rules.enabled(Rule::IOError) {
|
||||
let dummy =
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
|
||||
use ruff_linter::packaging;
|
||||
use ruff_linter::settings::flags;
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::diagnostics::{lint_stdin, Diagnostics};
|
||||
@@ -18,20 +18,20 @@ pub(crate) fn check_stdin(
|
||||
noqa: flags::Noqa,
|
||||
fix_mode: flags::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
if pyproject_config.settings.file_resolver.force_exclude {
|
||||
let mut resolver = Resolver::new(pyproject_config);
|
||||
|
||||
if resolver.force_exclude() {
|
||||
if let Some(filename) = filename {
|
||||
if !python_file_at_path(filename, pyproject_config, overrides)? {
|
||||
if !python_file_at_path(filename, &mut resolver, overrides)? {
|
||||
if fix_mode.is_apply() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
let lint_settings = &pyproject_config.settings.linter;
|
||||
if filename
|
||||
.file_name()
|
||||
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
|
||||
{
|
||||
if filename.file_name().is_some_and(|name| {
|
||||
match_exclusion(filename, name, &resolver.base_settings().linter.exclude)
|
||||
}) {
|
||||
if fix_mode.is_apply() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
@@ -41,13 +41,13 @@ pub(crate) fn check_stdin(
|
||||
}
|
||||
let stdin = read_from_stdin()?;
|
||||
let package_root = filename.and_then(Path::parent).and_then(|path| {
|
||||
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
|
||||
packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages)
|
||||
});
|
||||
let mut diagnostics = lint_stdin(
|
||||
filename,
|
||||
package_root,
|
||||
stdin,
|
||||
&pyproject_config.settings,
|
||||
resolver.base_settings(),
|
||||
noqa,
|
||||
fix_mode,
|
||||
)?;
|
||||
|
||||
@@ -25,9 +25,7 @@ use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
|
||||
};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
@@ -79,7 +77,7 @@ pub(crate) fn format(
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
|
||||
warn_incompatible_formatter_settings(&resolver);
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
let package_roots = resolver.package_roots(
|
||||
@@ -88,7 +86,6 @@ pub(crate) fn format(
|
||||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
&pyproject_config,
|
||||
);
|
||||
|
||||
let caches = if cli.no_cache {
|
||||
@@ -99,11 +96,7 @@ pub(crate) fn format(
|
||||
#[cfg(debug_assertions)]
|
||||
crate::warn_user!("Detected debug build without --no-cache.");
|
||||
|
||||
Some(PackageCacheMap::init(
|
||||
&pyproject_config,
|
||||
&package_roots,
|
||||
&resolver,
|
||||
))
|
||||
Some(PackageCacheMap::init(&package_roots, &resolver))
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
@@ -118,7 +111,7 @@ pub(crate) fn format(
|
||||
return None;
|
||||
};
|
||||
|
||||
let settings = resolver.resolve(path, &pyproject_config);
|
||||
let settings = resolver.resolve(path);
|
||||
|
||||
// Ignore files that are excluded from formatting
|
||||
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
|
||||
@@ -723,15 +716,10 @@ impl Display for FormatCommandError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn warn_incompatible_formatter_settings(
|
||||
pyproject_config: &PyprojectConfig,
|
||||
resolver: Option<&Resolver>,
|
||||
) {
|
||||
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
// First, collect all rules that are incompatible regardless of the linter-specific settings.
|
||||
let mut incompatible_rules = FxHashSet::default();
|
||||
for setting in std::iter::once(&pyproject_config.settings)
|
||||
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
|
||||
{
|
||||
for setting in resolver.settings() {
|
||||
for rule in [
|
||||
// The formatter might collapse implicit string concatenation on a single line.
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
@@ -760,9 +748,7 @@ pub(super) fn warn_incompatible_formatter_settings(
|
||||
}
|
||||
|
||||
// Next, validate settings-specific incompatibilities.
|
||||
for setting in std::iter::once(&pyproject_config.settings)
|
||||
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
|
||||
{
|
||||
for setting in resolver.settings() {
|
||||
// Validate all rules that rely on tab styles.
|
||||
if setting.linter.rules.enabled(Rule::TabIndentation)
|
||||
&& setting.formatter.indent_style.is_tab()
|
||||
|
||||
@@ -6,7 +6,7 @@ use log::error;
|
||||
|
||||
use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
@@ -27,24 +27,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
|
||||
warn_incompatible_formatter_settings(&pyproject_config, None);
|
||||
let mut resolver = Resolver::new(&pyproject_config);
|
||||
warn_incompatible_formatter_settings(&resolver);
|
||||
|
||||
let mode = FormatMode::from_cli(cli);
|
||||
|
||||
if pyproject_config.settings.file_resolver.force_exclude {
|
||||
if resolver.force_exclude() {
|
||||
if let Some(filename) = cli.stdin_filename.as_deref() {
|
||||
if !python_file_at_path(filename, &pyproject_config, overrides)? {
|
||||
if !python_file_at_path(filename, &mut resolver, overrides)? {
|
||||
if mode.is_write() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let format_settings = &pyproject_config.settings.formatter;
|
||||
if filename
|
||||
.file_name()
|
||||
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
|
||||
{
|
||||
if filename.file_name().is_some_and(|name| {
|
||||
match_exclusion(filename, name, &resolver.base_settings().formatter.exclude)
|
||||
}) {
|
||||
if mode.is_write() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
@@ -63,12 +62,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
};
|
||||
|
||||
// Format the file.
|
||||
match format_source_code(
|
||||
path,
|
||||
&pyproject_config.settings.formatter,
|
||||
source_type,
|
||||
mode,
|
||||
) {
|
||||
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
|
||||
@@ -18,6 +18,7 @@ struct Explanation<'a> {
|
||||
summary: &'a str,
|
||||
message_formats: &'a [&'a str],
|
||||
fix: String,
|
||||
#[allow(clippy::struct_field_names)]
|
||||
explanation: Option<&'a str>,
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn show_settings(
|
||||
bail!("No files found under the given path");
|
||||
};
|
||||
|
||||
let settings = resolver.resolve(&path, pyproject_config);
|
||||
let settings = resolver.resolve(&path);
|
||||
|
||||
writeln!(writer, "Resolved settings for: {path:?}")?;
|
||||
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::ops::{Add, AddAssign};
|
||||
@@ -273,6 +274,7 @@ pub(crate) fn lint_path(
|
||||
data: (messages, imports),
|
||||
error: parse_error,
|
||||
},
|
||||
transformed,
|
||||
fixed,
|
||||
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
|
||||
if let Ok(FixerResult {
|
||||
@@ -301,7 +303,12 @@ pub(crate) fn lint_path(
|
||||
flags::FixMode::Generate => {}
|
||||
}
|
||||
}
|
||||
(result, fixed)
|
||||
let transformed = if let Cow::Owned(transformed) = transformed {
|
||||
transformed
|
||||
} else {
|
||||
source_kind
|
||||
};
|
||||
(result, transformed, fixed)
|
||||
} else {
|
||||
// If we fail to fix, lint the original source code.
|
||||
let result = lint_only(
|
||||
@@ -313,8 +320,9 @@ pub(crate) fn lint_path(
|
||||
source_type,
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
(result, fixed)
|
||||
(result, transformed, fixed)
|
||||
}
|
||||
} else {
|
||||
let result = lint_only(
|
||||
@@ -326,8 +334,9 @@ pub(crate) fn lint_path(
|
||||
source_type,
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
(result, fixed)
|
||||
(result, transformed, fixed)
|
||||
};
|
||||
|
||||
let imports = imports.unwrap_or_default();
|
||||
@@ -335,7 +344,7 @@ pub(crate) fn lint_path(
|
||||
if let Some((cache, relative_path, key)) = caching {
|
||||
// We don't cache parsing errors.
|
||||
if parse_error.is_none() {
|
||||
// `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
|
||||
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
|
||||
// and writing the diff to stdout, respectively). If a file has diagnostics, we
|
||||
// need to avoid reading from and writing to the cache in these modes.
|
||||
if match fix_mode {
|
||||
@@ -350,7 +359,7 @@ pub(crate) fn lint_path(
|
||||
LintCacheData::from_messages(
|
||||
&messages,
|
||||
imports.clone(),
|
||||
source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
|
||||
transformed.as_ipy_notebook().map(Notebook::index).cloned(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -360,11 +369,11 @@ pub(crate) fn lint_path(
|
||||
if let Some(error) = parse_error {
|
||||
error!(
|
||||
"{}",
|
||||
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &source_kind,)
|
||||
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &transformed)
|
||||
);
|
||||
}
|
||||
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
@@ -415,6 +424,7 @@ pub(crate) fn lint_stdin(
|
||||
data: (messages, imports),
|
||||
error: parse_error,
|
||||
},
|
||||
transformed,
|
||||
fixed,
|
||||
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
|
||||
if let Ok(FixerResult {
|
||||
@@ -443,8 +453,12 @@ pub(crate) fn lint_stdin(
|
||||
}
|
||||
flags::FixMode::Generate => {}
|
||||
}
|
||||
|
||||
(result, fixed)
|
||||
let transformed = if let Cow::Owned(transformed) = transformed {
|
||||
transformed
|
||||
} else {
|
||||
source_kind
|
||||
};
|
||||
(result, transformed, fixed)
|
||||
} else {
|
||||
// If we fail to fix, lint the original source code.
|
||||
let result = lint_only(
|
||||
@@ -456,14 +470,15 @@ pub(crate) fn lint_stdin(
|
||||
source_type,
|
||||
ParseSource::None,
|
||||
);
|
||||
let fixed = FxHashMap::default();
|
||||
|
||||
// Write the contents to stdout anyway.
|
||||
if fix_mode.is_apply() {
|
||||
source_kind.write(&mut io::stdout().lock())?;
|
||||
}
|
||||
|
||||
(result, fixed)
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
(result, transformed, fixed)
|
||||
}
|
||||
} else {
|
||||
let result = lint_only(
|
||||
@@ -475,20 +490,21 @@ pub(crate) fn lint_stdin(
|
||||
source_type,
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
(result, fixed)
|
||||
(result, transformed, fixed)
|
||||
};
|
||||
|
||||
let imports = imports.unwrap_or_default();
|
||||
|
||||
if let Some(err) = parse_error {
|
||||
if let Some(error) = parse_error {
|
||||
error!(
|
||||
"Failed to parse {}: {err}",
|
||||
path.map_or_else(|| "-".into(), fs::relativize_path).bold()
|
||||
"{}",
|
||||
DisplayParseError::from_source_kind(error, path.map(Path::to_path_buf), &transformed)
|
||||
);
|
||||
}
|
||||
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
|
||||
FxHashMap::from_iter([(
|
||||
path.map_or_else(|| "-".into(), |path| path.to_string_lossy().to_string()),
|
||||
notebook.into_index(),
|
||||
|
||||
@@ -284,7 +284,8 @@ fn stdin_fix_jupyter() {
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os"
|
||||
"import os\n",
|
||||
"print(1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -302,7 +303,8 @@ fn stdin_fix_jupyter() {
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys"
|
||||
"import sys\n",
|
||||
"print(x)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -354,8 +356,8 @@ fn stdin_fix_jupyter() {
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{
|
||||
"cells": [
|
||||
@@ -365,7 +367,9 @@ fn stdin_fix_jupyter() {
|
||||
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
"source": [
|
||||
"print(1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -381,7 +385,9 @@ fn stdin_fix_jupyter() {
|
||||
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
"source": [
|
||||
"print(x)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -433,7 +439,8 @@ fn stdin_fix_jupyter() {
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
----- stderr -----
|
||||
Found 2 errors (2 fixed, 0 remaining).
|
||||
Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
|
||||
Found 3 errors (2 fixed, 1 remaining).
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -719,6 +726,22 @@ fn stdin_format_jupyter() {
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_parse_error() {
|
||||
let mut cmd = RuffCheck::default().build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("from foo import =\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:17: E999 SyntaxError: Unexpected token '='
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse at 1:17: Unexpected token '='
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_source() {
|
||||
let mut cmd = RuffCheck::default().args(["--show-source"]).build();
|
||||
@@ -743,6 +766,7 @@ fn show_source() {
|
||||
fn explain_status_codes_f401() {
|
||||
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "F401"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explain_status_codes_ruf404() {
|
||||
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "RUF404"]), @r###"
|
||||
|
||||
@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use ruff_cli::args::{FormatCommand, LogLevelArgs};
|
||||
use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
|
||||
use ruff_cli::resolve::resolve;
|
||||
use ruff_formatter::{FormatError, LineWidth, PrintError};
|
||||
use ruff_linter::logging::LogLevel;
|
||||
@@ -38,24 +38,24 @@ use ruff_python_formatter::{
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||
|
||||
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn ruff_check_paths(
|
||||
dirs: &[PathBuf],
|
||||
) -> anyhow::Result<(
|
||||
Vec<Result<ResolvedFile, ignore::Error>>,
|
||||
Resolver,
|
||||
PyprojectConfig,
|
||||
)> {
|
||||
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
|
||||
let args_matches = FormatCommand::command()
|
||||
.no_binary_name(true)
|
||||
.get_matches_from(dirs);
|
||||
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
|
||||
let (cli, overrides) = arguments.partition();
|
||||
Ok((cli, overrides))
|
||||
}
|
||||
|
||||
/// Find the [`PyprojectConfig`] to use for formatting.
|
||||
fn find_pyproject_config(
|
||||
cli: &FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
) -> anyhow::Result<PyprojectConfig> {
|
||||
let mut pyproject_config = resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
&overrides,
|
||||
overrides,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
// We don't want to format pyproject.toml
|
||||
@@ -64,11 +64,18 @@ fn ruff_check_paths(
|
||||
FilePattern::Builtin("*.pyi"),
|
||||
])
|
||||
.unwrap();
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?;
|
||||
if paths.is_empty() {
|
||||
bail!("no python files in {:?}", dirs)
|
||||
}
|
||||
Ok((paths, resolver, pyproject_config))
|
||||
Ok(pyproject_config)
|
||||
}
|
||||
|
||||
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn ruff_check_paths<'a>(
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
cli: &FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
|
||||
Ok((paths, resolver))
|
||||
}
|
||||
|
||||
/// Collects statistics over the formatted files to compute the Jaccard index or the similarity
|
||||
@@ -216,6 +223,7 @@ pub(crate) struct Args {
|
||||
#[arg(long)]
|
||||
pub(crate) files_with_errors: Option<u32>,
|
||||
#[clap(flatten)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub(crate) log_level_args: LogLevelArgs,
|
||||
}
|
||||
|
||||
@@ -451,11 +459,17 @@ fn format_dev_project(
|
||||
files[0].display()
|
||||
);
|
||||
|
||||
// TODO(konstin): black excludes
|
||||
// TODO(konstin): Respect black's excludes.
|
||||
|
||||
// Find files to check (or in this case, format twice). Adapted from ruff_cli
|
||||
// First argument is ignored
|
||||
let (paths, resolver, pyproject_config) = ruff_check_paths(files)?;
|
||||
let (cli, overrides) = parse_cli(files)?;
|
||||
let pyproject_config = find_pyproject_config(&cli, &overrides)?;
|
||||
let (paths, resolver) = ruff_check_paths(&pyproject_config, &cli, &overrides)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
bail!("No Python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let results = {
|
||||
let pb_span =
|
||||
@@ -468,14 +482,7 @@ fn format_dev_project(
|
||||
#[cfg(feature = "singlethreaded")]
|
||||
let iter = { paths.into_iter() };
|
||||
iter.map(|path| {
|
||||
let result = format_dir_entry(
|
||||
path,
|
||||
stability_check,
|
||||
write,
|
||||
&black_options,
|
||||
&resolver,
|
||||
&pyproject_config,
|
||||
);
|
||||
let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
|
||||
pb_span.pb_inc(1);
|
||||
result
|
||||
})
|
||||
@@ -525,14 +532,13 @@ fn format_dev_project(
|
||||
})
|
||||
}
|
||||
|
||||
/// Error handling in between walkdir and `format_dev_file`
|
||||
/// Error handling in between walkdir and `format_dev_file`.
|
||||
fn format_dir_entry(
|
||||
resolved_file: Result<ResolvedFile, ignore::Error>,
|
||||
stability_check: bool,
|
||||
write: bool,
|
||||
options: &BlackOptions,
|
||||
resolver: &Resolver,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
|
||||
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
|
||||
// For some reason it does not filter in the beginning
|
||||
@@ -543,7 +549,7 @@ fn format_dir_entry(
|
||||
let path = resolved_file.into_path();
|
||||
let mut options = options.to_py_format_options(&path);
|
||||
|
||||
let settings = resolver.resolve(&path, pyproject_config);
|
||||
let settings = resolver.resolve(&path);
|
||||
// That's a bad way of doing this but it's not worth doing something better for format_dev
|
||||
if settings.formatter.line_width != LineWidth::default() {
|
||||
options = options.with_line_width(settings.formatter.line_width);
|
||||
|
||||
@@ -1472,6 +1472,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
}
|
||||
|
||||
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
|
||||
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
|
||||
fits.state.line_width > fits.options().line_width.into()
|
||||
&& !args.measure_mode().allows_text_overflow()
|
||||
}
|
||||
|
||||
let indent = std::mem::take(&mut self.state.pending_indent);
|
||||
self.state.line_width +=
|
||||
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
|
||||
@@ -1493,7 +1498,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
return Fits::No;
|
||||
}
|
||||
match args.measure_mode() {
|
||||
MeasureMode::FirstLine => return Fits::Yes,
|
||||
MeasureMode::FirstLine => {
|
||||
return if exceeds_width(self, args) {
|
||||
Fits::No
|
||||
} else {
|
||||
Fits::Yes
|
||||
};
|
||||
}
|
||||
MeasureMode::AllLines
|
||||
| MeasureMode::AllLinesAllowTextOverflow => {
|
||||
self.state.line_width = 0;
|
||||
@@ -1511,9 +1522,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.line_width > self.options().line_width.into()
|
||||
&& !args.measure_mode().allows_text_overflow()
|
||||
{
|
||||
if exceeds_width(self, args) {
|
||||
return Fits::No;
|
||||
}
|
||||
|
||||
|
||||
@@ -264,3 +264,41 @@ def func(x: int):
|
||||
if x > 0:
|
||||
return 1
|
||||
raise ValueError
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if x > 5:
|
||||
raise ValueError
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if x > 5:
|
||||
raise ValueError
|
||||
elif x > 10:
|
||||
pass
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if x > 5:
|
||||
raise ValueError
|
||||
elif x > 10:
|
||||
return 5
|
||||
|
||||
|
||||
def func():
|
||||
try:
|
||||
return 5
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValueError
|
||||
|
||||
|
||||
def func(x: int):
|
||||
match x:
|
||||
case [1, 2, 3]:
|
||||
return 1
|
||||
case y:
|
||||
return "foo"
|
||||
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S401.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S401.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import telnetlib # S401
|
||||
from telnetlib import Telnet # S401
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S402.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S402.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import ftplib # S402
|
||||
from ftplib import FTP # S402
|
||||
8
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S403.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S403.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import dill # S403
|
||||
from dill import objects # S403
|
||||
import shelve
|
||||
from shelve import open
|
||||
import cPickle
|
||||
from cPickle import load
|
||||
import pickle
|
||||
from pickle import load
|
||||
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S404.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S404.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import subprocess # S404
|
||||
from subprocess import Popen # S404
|
||||
from subprocess import Popen as pop # S404
|
||||
4
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S405.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S405.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import xml.etree.cElementTree # S405
|
||||
from xml.etree import cElementTree # S405
|
||||
import xml.etree.ElementTree # S405
|
||||
from xml.etree import ElementTree # S405
|
||||
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S406.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S406.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from xml import sax # S406
|
||||
import xml.sax as xmls # S406
|
||||
import xml.sax # S406
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S407.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S407.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from xml.dom import expatbuilder # S407
|
||||
import xml.dom.expatbuilder # S407
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from xml.dom.minidom import parseString # S408
|
||||
import xml.dom.minidom # S408
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S409.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S409.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from xml.dom.pulldom import parseString # S409
|
||||
import xml.dom.pulldom # S409
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S410.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S410.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import lxml # S410
|
||||
from lxml import etree # S410
|
||||
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S411.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S411.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import xmlrpc # S411
|
||||
from xmlrpc import server # S411
|
||||
1
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S412.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S412.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
from twisted.web.twcgi import CGIScript # S412
|
||||
4
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S413.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S413.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import Crypto.Hash # S413
|
||||
from Crypto.Hash import MD2 # S413
|
||||
import Crypto.PublicKey # S413
|
||||
from Crypto.PublicKey import RSA # S413
|
||||
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S415.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S415.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import pyghmi # S415
|
||||
from pyghmi import foo # S415
|
||||
|
||||
16
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S502.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S502.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import ssl
|
||||
from ssl import wrap_socket
|
||||
from OpenSSL import SSL
|
||||
from OpenSSL.SSL import Context
|
||||
|
||||
wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
|
||||
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
|
||||
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
Context(method=SSL.SSLv3_METHOD) # S502
|
||||
Context(method=SSL.TLSv1_METHOD) # S502
|
||||
|
||||
wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
|
||||
SSL.Context(method=SSL.TLS_SERVER_METHOD) # OK
|
||||
func(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|
||||
23
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S503.py
vendored
Normal file
23
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S503.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import ssl
|
||||
from OpenSSL import SSL
|
||||
from ssl import PROTOCOL_TLSv1
|
||||
|
||||
|
||||
def func(version=ssl.PROTOCOL_SSLv2): # S503
|
||||
pass
|
||||
|
||||
|
||||
def func(protocol=SSL.SSLv2_METHOD): # S503
|
||||
pass
|
||||
|
||||
|
||||
def func(version=SSL.SSLv23_METHOD): # S503
|
||||
pass
|
||||
|
||||
|
||||
def func(protocol=PROTOCOL_TLSv1): # S503
|
||||
pass
|
||||
|
||||
|
||||
def func(version=SSL.TLSv1_2_METHOD): # OK
|
||||
pass
|
||||
15
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S504.py
vendored
Normal file
15
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S504.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import ssl
|
||||
from ssl import wrap_socket
|
||||
|
||||
ssl.wrap_socket() # S504
|
||||
wrap_socket() # S504
|
||||
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|
||||
|
||||
|
||||
class Class:
|
||||
def wrap_socket(self):
|
||||
pass
|
||||
|
||||
|
||||
obj = Class()
|
||||
obj.wrap_socket() # OK
|
||||
@@ -71,6 +71,8 @@ foo.is_(True)
|
||||
bar.is_not(False)
|
||||
next(iter([]), False)
|
||||
sa.func.coalesce(tbl.c.valid, False)
|
||||
setVisible(True)
|
||||
set_visible(True)
|
||||
|
||||
|
||||
class Registry:
|
||||
@@ -114,3 +116,6 @@ from typing import override
|
||||
@override
|
||||
def func(x: bool):
|
||||
pass
|
||||
|
||||
|
||||
settings(True)
|
||||
|
||||
@@ -8,46 +8,14 @@ class MyClass:
|
||||
self.id = 10
|
||||
self.dir = "."
|
||||
|
||||
def str(self):
|
||||
pass
|
||||
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class MyClass(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
from threading import Event
|
||||
|
||||
|
||||
class CustomEvent(Event):
|
||||
def set(self) -> None:
|
||||
...
|
||||
|
||||
def str(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
from logging import Filter, LogRecord
|
||||
|
||||
|
||||
class CustomFilter(Filter):
|
||||
def filter(self, record: LogRecord) -> bool:
|
||||
...
|
||||
|
||||
def str(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class MyClass:
|
||||
@override
|
||||
def str(self):
|
||||
pass
|
||||
|
||||
def int(self):
|
||||
pass
|
||||
|
||||
def str(self):
|
||||
pass
|
||||
|
||||
def method_usage(self) -> str:
|
||||
pass
|
||||
|
||||
def attribute_usage(self) -> id:
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from typing import Protocol
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
|
||||
class _Foo(Protocol):
|
||||
@@ -10,9 +10,23 @@ class _Bar(typing.Protocol):
|
||||
bar: int
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class _Baz(Protocol[_T]):
|
||||
x: _T
|
||||
|
||||
|
||||
# OK
|
||||
class _UsedPrivateProtocol(Protocol):
|
||||
bar: int
|
||||
|
||||
|
||||
def uses__UsedPrivateProtocol(arg: _UsedPrivateProtocol) -> None: ...
|
||||
# Also OK
|
||||
class _UsedGenericPrivateProtocol(Protocol[_T]):
|
||||
x: _T
|
||||
|
||||
|
||||
def uses_some_private_protocols(
|
||||
arg: _UsedPrivateProtocol, arg2: _UsedGenericPrivateProtocol[int]
|
||||
) -> None: ...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from typing import Protocol
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
|
||||
class _Foo(object, Protocol):
|
||||
@@ -10,9 +10,23 @@ class _Bar(typing.Protocol):
|
||||
bar: int
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class _Baz(Protocol[_T]):
|
||||
x: _T
|
||||
|
||||
|
||||
# OK
|
||||
class _UsedPrivateProtocol(Protocol):
|
||||
bar: int
|
||||
|
||||
|
||||
def uses__UsedPrivateProtocol(arg: _UsedPrivateProtocol) -> None: ...
|
||||
# Also OK
|
||||
class _UsedGenericPrivateProtocol(Protocol[_T]):
|
||||
x: _T
|
||||
|
||||
|
||||
def uses_some_private_protocols(
|
||||
arg: _UsedPrivateProtocol, arg2: _UsedGenericPrivateProtocol[int]
|
||||
) -> None: ...
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import warnings
|
||||
import typing_extensions
|
||||
from collections.abc import Callable
|
||||
from warnings import deprecated
|
||||
|
||||
|
||||
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None:
|
||||
...
|
||||
|
||||
@@ -45,3 +51,24 @@ class Demo:
|
||||
|
||||
def func() -> None:
|
||||
"""Docstrings are excluded from this rule. Some padding."""
|
||||
|
||||
|
||||
@warnings.deprecated("Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay")
|
||||
def deprecated_function() -> None: ...
|
||||
|
||||
|
||||
@typing_extensions.deprecated("Another loooooooooooooooooooooong deprecation message, it's still okay")
|
||||
def another_deprecated_function() -> None: ...
|
||||
|
||||
|
||||
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message")
|
||||
def a_third_deprecated_function() -> None: ...
|
||||
|
||||
|
||||
def not_warnings_dot_deprecated(
|
||||
msg: str
|
||||
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
|
||||
|
||||
|
||||
@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
|
||||
def not_a_deprecated_function() -> None: ...
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import warnings
|
||||
import typing_extensions
|
||||
from typing_extensions import deprecated
|
||||
|
||||
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
|
||||
def f2(
|
||||
x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
|
||||
@@ -38,3 +42,25 @@ class Demo:
|
||||
|
||||
def func() -> None:
|
||||
"""Docstrings are excluded from this rule. Some padding.""" # OK
|
||||
|
||||
@warnings.deprecated(
|
||||
"Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay" # OK
|
||||
)
|
||||
def deprecated_function() -> None: ...
|
||||
|
||||
@typing_extensions.deprecated(
|
||||
"Another loooooooooooooooooooooong deprecation message, it's still okay" # OK
|
||||
)
|
||||
def another_deprecated_function() -> None: ...
|
||||
|
||||
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message") # OK
|
||||
def a_third_deprecated_function() -> None: ...
|
||||
|
||||
def not_warnings_dot_deprecated(
|
||||
msg: str
|
||||
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
|
||||
|
||||
@not_warnings_dot_deprecated(
|
||||
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
|
||||
)
|
||||
def not_a_deprecated_function() -> None: ...
|
||||
|
||||
@@ -1,82 +1,174 @@
|
||||
import collections.abc
|
||||
import typing
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from typing import Any
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorReturningSimpleGenerator1:
|
||||
def __iter__(self) -> Generator: # PYI058 (use `Iterator`)
|
||||
return (x for x in range(42))
|
||||
class IteratorReturningSimpleGenerator1:
|
||||
def __iter__(self) -> Generator:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
class IteratorReturningSimpleGenerator2:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: # PYI058 (use `Iterator`)
|
||||
"""Fully documented, because I'm a runtime function!"""
|
||||
yield from "abcdefg"
|
||||
return None
|
||||
|
||||
class IteratorReturningSimpleGenerator3:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: # PYI058 (use `Iterator`)
|
||||
yield "a"
|
||||
yield "b"
|
||||
yield "c"
|
||||
return
|
||||
def scope():
|
||||
import typing
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator1:
|
||||
def __aiter__(self) -> typing.AsyncGenerator: pass # PYI058 (Use `AsyncIterator`)
|
||||
class IteratorReturningSimpleGenerator2:
|
||||
def __iter__(self) -> typing.Generator:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator2:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]: ... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator3:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: pass # PYI058 (Use `AsyncIterator`)
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class CorrectIterator:
|
||||
def __iter__(self) -> Iterator[str]: ... # OK
|
||||
class IteratorReturningSimpleGenerator3:
|
||||
def __iter__(self) -> collections.abc.Generator:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
class CorrectAsyncIterator:
|
||||
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
|
||||
|
||||
class Fine:
|
||||
def __iter__(self) -> typing.Self: ... # OK
|
||||
def scope():
|
||||
import collections.abc
|
||||
from typing import Any
|
||||
|
||||
class StrangeButWeWontComplainHere:
|
||||
def __aiter__(self) -> list[bytes]: ... # OK
|
||||
class IteratorReturningSimpleGenerator4:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
def __iter__(self) -> Generator: ... # OK (not in class scope)
|
||||
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
|
||||
|
||||
class IteratorReturningComplexGenerator:
|
||||
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
|
||||
def scope():
|
||||
import collections.abc
|
||||
import typing
|
||||
|
||||
class AsyncIteratorReturningComplexAsyncGenerator:
|
||||
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
|
||||
class IteratorReturningSimpleGenerator5:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
class ClassWithInvalidAsyncAiterMethod:
|
||||
async def __aiter__(self) -> AsyncGenerator: ... # OK
|
||||
|
||||
class IteratorWithUnusualParameters1:
|
||||
def __iter__(self, foo) -> Generator: ... # OK
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters2:
|
||||
def __iter__(self, *, bar) -> Generator: ... # OK
|
||||
class IteratorReturningSimpleGenerator6:
|
||||
def __iter__(self, /) -> Generator[str, None, None]:
|
||||
... # PYI058 (use `Iterator`)
|
||||
|
||||
class IteratorWithUnusualParameters3:
|
||||
def __iter__(self, *args) -> Generator: ... # OK
|
||||
|
||||
class IteratorWithUnusualParameters4:
|
||||
def __iter__(self, **kwargs) -> Generator: ... # OK
|
||||
def scope():
|
||||
import typing_extensions
|
||||
|
||||
class IteratorWithIterMethodThatReturnsThings:
|
||||
def __iter__(self) -> Generator: # OK
|
||||
yield
|
||||
return 42
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator1:
|
||||
def __aiter__(
|
||||
self,
|
||||
) -> typing_extensions.AsyncGenerator:
|
||||
... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
class IteratorWithIterMethodThatReceivesThingsFromSend:
|
||||
def __iter__(self) -> Generator: # OK
|
||||
x = yield 42
|
||||
|
||||
class IteratorWithNonTrivialIterBody:
|
||||
def __iter__(self) -> Generator: # OK
|
||||
foo, bar, baz = (1, 2, 3)
|
||||
yield foo
|
||||
yield bar
|
||||
yield baz
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator2:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]:
|
||||
... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator3:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]:
|
||||
... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
|
||||
def scope():
|
||||
from typing import Iterator
|
||||
|
||||
class CorrectIterator:
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class CorrectAsyncIterator:
|
||||
def __aiter__(self) -> collections.abc.AsyncIterator[int]:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
import typing
|
||||
|
||||
class Fine:
|
||||
def __iter__(self) -> typing.Self:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
class StrangeButWeWontComplainHere:
|
||||
def __aiter__(self) -> list[bytes]:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
def __iter__(self) -> Generator:
|
||||
... # OK (not in class scope)
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
def __aiter__(self) -> AsyncGenerator:
|
||||
... # OK (not in class scope)
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorReturningComplexGenerator:
|
||||
def __iter__(self) -> Generator[str, int, bytes]:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
class AsyncIteratorReturningComplexAsyncGenerator:
|
||||
def __aiter__(self) -> AsyncGenerator[str, int]:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
class ClassWithInvalidAsyncAiterMethod:
|
||||
async def __aiter__(self) -> AsyncGenerator:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters1:
|
||||
def __iter__(self, foo) -> Generator:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters2:
|
||||
def __iter__(self, *, bar) -> Generator:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters3:
|
||||
def __iter__(self, *args) -> Generator:
|
||||
... # OK
|
||||
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters4:
|
||||
def __iter__(self, **kwargs) -> Generator:
|
||||
... # OK
|
||||
|
||||
@@ -1,58 +1,128 @@
|
||||
import collections.abc
|
||||
import typing
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from typing import Any
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorReturningSimpleGenerator1:
|
||||
def __iter__(self) -> Generator: ... # PYI058 (use `Iterator`)
|
||||
class IteratorReturningSimpleGenerator1:
|
||||
def __iter__(self) -> Generator: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class IteratorReturningSimpleGenerator2:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: ... # PYI058 (use `Iterator`)
|
||||
def scope():
|
||||
import typing
|
||||
|
||||
class IteratorReturningSimpleGenerator3:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: ... # PYI058 (use `Iterator`)
|
||||
class IteratorReturningSimpleGenerator2:
|
||||
def __iter__(self) -> typing.Generator: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator1:
|
||||
def __aiter__(self) -> typing.AsyncGenerator: ... # PYI058 (Use `AsyncIterator`)
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator2:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]: ... # PYI058 (Use `AsyncIterator`)
|
||||
class IteratorReturningSimpleGenerator3:
|
||||
def __iter__(self) -> collections.abc.Generator: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator3:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: ... # PYI058 (Use `AsyncIterator`)
|
||||
def scope():
|
||||
import collections.abc
|
||||
from typing import Any
|
||||
|
||||
class CorrectIterator:
|
||||
def __iter__(self) -> Iterator[str]: ... # OK
|
||||
class IteratorReturningSimpleGenerator4:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class CorrectAsyncIterator:
|
||||
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
|
||||
def scope():
|
||||
import collections.abc
|
||||
import typing
|
||||
|
||||
class Fine:
|
||||
def __iter__(self) -> typing.Self: ... # OK
|
||||
class IteratorReturningSimpleGenerator5:
|
||||
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class StrangeButWeWontComplainHere:
|
||||
def __aiter__(self) -> list[bytes]: ... # OK
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
def __iter__(self) -> Generator: ... # OK (not in class scope)
|
||||
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
|
||||
class IteratorReturningSimpleGenerator6:
|
||||
def __iter__(self, /) -> Generator[str, None, None]: ... # PYI058 (use `Iterator`)
|
||||
|
||||
class IteratorReturningComplexGenerator:
|
||||
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
|
||||
def scope():
|
||||
import typing_extensions
|
||||
|
||||
class AsyncIteratorReturningComplexAsyncGenerator:
|
||||
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator1:
|
||||
def __aiter__(self,) -> typing_extensions.AsyncGenerator: ... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
class ClassWithInvalidAsyncAiterMethod:
|
||||
async def __aiter__(self) -> AsyncGenerator: ... # OK
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class IteratorWithUnusualParameters1:
|
||||
def __iter__(self, foo) -> Generator: ... # OK
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator3:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]:
|
||||
... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
class IteratorWithUnusualParameters2:
|
||||
def __iter__(self, *, bar) -> Generator: ... # OK
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class IteratorWithUnusualParameters3:
|
||||
def __iter__(self, *args) -> Generator: ... # OK
|
||||
class AsyncIteratorReturningSimpleAsyncGenerator3:
|
||||
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: ... # PYI058 (Use `AsyncIterator`)
|
||||
|
||||
class IteratorWithUnusualParameters4:
|
||||
def __iter__(self, **kwargs) -> Generator: ... # OK
|
||||
def scope():
|
||||
from typing import Iterator
|
||||
|
||||
class CorrectIterator:
|
||||
def __iter__(self) -> Iterator[str]: ... # OK
|
||||
|
||||
def scope():
|
||||
import collections.abc
|
||||
|
||||
class CorrectAsyncIterator:
|
||||
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
|
||||
|
||||
def scope():
|
||||
import typing
|
||||
|
||||
class Fine:
|
||||
def __iter__(self) -> typing.Self: ... # OK
|
||||
|
||||
def scope():
|
||||
class StrangeButWeWontComplainHere:
|
||||
def __aiter__(self) -> list[bytes]: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
def __iter__(self) -> Generator: ... # OK (not in class scope)
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorReturningComplexGenerator:
|
||||
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
class AsyncIteratorReturningComplexAsyncGenerator:
|
||||
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
class ClassWithInvalidAsyncAiterMethod:
|
||||
async def __aiter__(self) -> AsyncGenerator: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters1:
|
||||
def __iter__(self, foo) -> Generator: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters2:
|
||||
def __iter__(self, *, bar) -> Generator: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters3:
|
||||
def __iter__(self, *args) -> Generator: ... # OK
|
||||
|
||||
def scope():
|
||||
from collections.abc import Generator
|
||||
|
||||
class IteratorWithUnusualParameters4:
|
||||
def __iter__(self, **kwargs) -> Generator: ... # OK
|
||||
|
||||
23
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py
vendored
Normal file
23
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
def foo(d: dict[str, str]) -> None:
|
||||
for k, v in zip(d.keys(), d.values()): # SIM911
|
||||
...
|
||||
|
||||
for k, v in zip(d.keys(), d.values(), strict=True): # SIM911
|
||||
...
|
||||
|
||||
for k, v in zip(d.keys(), d.values(), struct=True): # OK
|
||||
...
|
||||
|
||||
|
||||
d1 = d2 = {}
|
||||
|
||||
for k, v in zip(d1.keys(), d2.values()): # OK
|
||||
...
|
||||
|
||||
for k, v in zip(d1.items(), d2.values()): # OK
|
||||
...
|
||||
|
||||
for k, v in zip(d2.keys(), d2.values()): # SIM911
|
||||
...
|
||||
|
||||
items = zip(x.keys(), x.values()) # OK
|
||||
@@ -51,3 +51,5 @@ if (True) == TrueElement or x == TrueElement:
|
||||
assert (not foo) in bar
|
||||
assert {"x": not foo} in bar
|
||||
assert [42, not foo] in bar
|
||||
|
||||
assert x in c > 0 == None
|
||||
|
||||
@@ -150,3 +150,21 @@ class Test:
|
||||
Args:
|
||||
arg1: some description of arg
|
||||
"""
|
||||
|
||||
|
||||
def select_data(
|
||||
query: str,
|
||||
args: tuple,
|
||||
database: str,
|
||||
auto_save: bool,
|
||||
) -> None:
|
||||
"""This function has an argument `args`, which shouldn't be mistaken for a section.
|
||||
|
||||
Args:
|
||||
query:
|
||||
Query template.
|
||||
args:
|
||||
A list of arguments.
|
||||
database:
|
||||
Which database to connect to ("origin" or "destination").
|
||||
"""
|
||||
|
||||
@@ -536,9 +536,29 @@ def non_empty_blank_line_before_section(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
The function's description.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def lowercase_sub_section_header():
|
||||
"""Below, `returns:` should _not_ be considered a section header.
|
||||
|
||||
Args:
|
||||
Here's a note.
|
||||
|
||||
returns:
|
||||
"""
|
||||
|
||||
|
||||
def titlecase_sub_section_header():
|
||||
"""Below, `Returns:` should be considered a section header.
|
||||
|
||||
Args:
|
||||
Here's a note.
|
||||
|
||||
Returns:
|
||||
"""
|
||||
|
||||
60
crates/ruff_linter/resources/test/fixtures/pyflakes/F402.ipynb
vendored
Normal file
60
crates/ruff_linter/resources/test/fixtures/pyflakes/F402.ipynb
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import os.path as path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "481fb4bf-c1b9-47da-927f-3cfdfe4b49ec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for os in range(3):\n",
|
||||
" pass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for path in range(3):\n",
|
||||
" pass"
|
||||
],
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"id": "2f0c65a5-0a0e-4080-afce-5a8ed0d706df"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff-playground)",
|
||||
"language": "python",
|
||||
"name": "ruff-playground"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
6
crates/ruff_linter/resources/test/fixtures/pyflakes/F811_27.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/pyflakes/F811_27.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
x = 1
|
||||
|
||||
if x > 0:
|
||||
import os
|
||||
30
crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py
vendored
Normal file
30
crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
print((3.0).__add__(4.0)) # PLC2801
|
||||
print((3.0).__sub__(4.0)) # PLC2801
|
||||
print((3.0).__mul__(4.0)) # PLC2801
|
||||
print((3.0).__truediv__(4.0)) # PLC2801
|
||||
print((3.0).__floordiv__(4.0)) # PLC2801
|
||||
print((3.0).__mod__(4.0)) # PLC2801
|
||||
print((3.0).__eq__(4.0)) # PLC2801
|
||||
print((3.0).__ne__(4.0)) # PLC2801
|
||||
print((3.0).__lt__(4.0)) # PLC2801
|
||||
print((3.0).__le__(4.0)) # PLC2801
|
||||
print((3.0).__gt__(4.0)) # PLC2801
|
||||
print((3.0).__ge__(4.0)) # PLC2801
|
||||
print((3.0).__str__()) # PLC2801
|
||||
print((3.0).__repr__()) # PLC2801
|
||||
print([1, 2, 3].__len__()) # PLC2801
|
||||
print((1).__neg__()) # PLC2801
|
||||
|
||||
|
||||
class Thing:
|
||||
def __init__(self, stuff: Any) -> None:
|
||||
super().__init__() # OK
|
||||
super().__class__(stuff=(1, 2, 3)) # OK
|
||||
|
||||
|
||||
blah = lambda: {"a": 1}.__delitem__("a") # OK
|
||||
|
||||
blah = dict[{"a": 1}.__delitem__("a")] # OK
|
||||
@@ -91,3 +91,12 @@ from typing_extensions import dataclass_transform
|
||||
|
||||
# UP035
|
||||
from backports.strenum import StrEnum
|
||||
|
||||
# UP035
|
||||
from typing_extensions import override
|
||||
|
||||
# UP035
|
||||
from typing_extensions import Buffer
|
||||
|
||||
# UP035
|
||||
from typing_extensions import get_original_bases
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
data = ["some", "Data"]
|
||||
constant = 5
|
||||
|
||||
# Ok
|
||||
# OK
|
||||
{value: value.upper() for value in data}
|
||||
{value.lower(): value.upper() for value in data}
|
||||
{v: v*v for v in range(10)}
|
||||
{(0, "a", v): v*v for v in range(10)} # Tuple with variable
|
||||
{v: v * v for v in range(10)}
|
||||
{(0, "a", v): v * v for v in range(10)} # Tuple with variable
|
||||
{constant: value.upper() for value in data for constant in data}
|
||||
{value.attribute: value.upper() for value in data for constant in data}
|
||||
{constant[value]: value.upper() for value in data for constant in data}
|
||||
{value[constant]: value.upper() for value in data for constant in data}
|
||||
|
||||
# Errors
|
||||
{"key": value.upper() for value in data}
|
||||
{True: value.upper() for value in data}
|
||||
{0: value.upper() for value in data}
|
||||
{(1, "a"): value.upper() for value in data} # constant tuple
|
||||
{(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
{constant: value.upper() for value in data}
|
||||
{constant + constant: value.upper() for value in data}
|
||||
{constant.attribute: value.upper() for value in data}
|
||||
{constant[0]: value.upper() for value in data}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from typing import Annotated, Any, Literal, Optional, Tuple, Union
|
||||
from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
|
||||
|
||||
|
||||
def f(arg: int):
|
||||
@@ -257,3 +257,13 @@ from custom_typing import MaybeInt
|
||||
|
||||
def f(arg: MaybeInt = None):
|
||||
pass
|
||||
|
||||
|
||||
# Hashable
|
||||
|
||||
def f(arg: Hashable = None): # OK
|
||||
pass
|
||||
|
||||
|
||||
def f(arg: Hashable | int = None): # OK
|
||||
pass
|
||||
|
||||
114
crates/ruff_linter/resources/test/fixtures/ruff/RUF021.py
vendored
Normal file
114
crates/ruff_linter/resources/test/fixtures/ruff/RUF021.py
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# See https://docs.python.org/3/reference/expressions.html#operator-precedence
|
||||
# for the official docs on operator precedence.
|
||||
#
|
||||
# Most importantly, `and` *always* takes precedence over `or`.
|
||||
#
|
||||
# `not` (the third boolean/logical operator) takes precedence over both,
|
||||
# but the rule there is easier to remember,
|
||||
# so we don't emit a diagnostic if a `not` expression is unparenthesized
|
||||
# as part of a chain.
|
||||
|
||||
a, b, c = 1, 0, 2
|
||||
x = a or b and c # RUF021: => `a or (b and c)`
|
||||
x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
|
||||
a, b, c = 0, 1, 2
|
||||
y = a and b or c # RUF021: => `(a and b) or c`
|
||||
|
||||
a, b, c, d = 1, 2, 0, 3
|
||||
if a or b or c and d: # RUF021: => `a or b or (c and d)`
|
||||
pass
|
||||
|
||||
a, b, c, d = 0, 0, 2, 3
|
||||
|
||||
if bool():
|
||||
pass
|
||||
elif a or b and c or d: # RUF021: => `a or (b and c) or d`
|
||||
pass
|
||||
|
||||
a, b, c, d = 0, 1, 0, 2
|
||||
while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
pass
|
||||
|
||||
b, c, d, e = 2, 3, 0, 4
|
||||
# RUF021: => `a or b or c or (d and e)`:
|
||||
z = [a for a in range(5) if a or b or c or d and e]
|
||||
|
||||
a, b, c, d = 0, 1, 3, 0
|
||||
assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
|
||||
if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
if (not a and b) or c or d: # OK
|
||||
pass
|
||||
|
||||
if (
|
||||
some_reasonably_long_condition
|
||||
or some_other_reasonably_long_condition
|
||||
and some_third_reasonably_long_condition
|
||||
or some_fourth_reasonably_long_condition
|
||||
and some_fifth_reasonably_long_condition
|
||||
# a commment
|
||||
and some_sixth_reasonably_long_condition
|
||||
and some_seventh_reasonably_long_condition
|
||||
# another comment
|
||||
or some_eighth_reasonably_long_condition
|
||||
):
|
||||
pass
|
||||
|
||||
#############################################
|
||||
# If they're all the same operator, it's fine
|
||||
#############################################
|
||||
|
||||
x = not a and c # OK
|
||||
|
||||
if a or b or c: # OK
|
||||
pass
|
||||
|
||||
while a and b and c: # OK
|
||||
pass
|
||||
|
||||
###########################################################
|
||||
# We don't consider `not` as part of a chain as problematic
|
||||
###########################################################
|
||||
|
||||
x = not a or not b or not c # OK
|
||||
|
||||
#####################################
|
||||
# If they're parenthesized, it's fine
|
||||
#####################################
|
||||
|
||||
a, b, c = 1, 0, 2
|
||||
x = a or (b and c) # OK
|
||||
x2 = (a or b) and c # OK
|
||||
x3 = (a or b) or c # OK
|
||||
x4 = (a and b) and c # OK
|
||||
|
||||
a, b, c = 0, 1, 2
|
||||
y = (a and b) or c # OK
|
||||
yy = a and (b or c) # OK
|
||||
|
||||
a, b, c, d = 1, 2, 0, 3
|
||||
if a or b or (c and d): # OK
|
||||
pass
|
||||
|
||||
a, b, c, d = 0, 0, 2, 3
|
||||
|
||||
if bool():
|
||||
pass
|
||||
elif a or (b and c) or d: # OK
|
||||
pass
|
||||
|
||||
a, b, c, d = 0, 1, 0, 2
|
||||
while (a and b) or (c and d): # OK
|
||||
pass
|
||||
|
||||
b, c, d, e = 2, 3, 0, 4
|
||||
z = [a for a in range(5) if a or b or c or (d and e)] # OK
|
||||
|
||||
a, b = 1, 2
|
||||
if (not a) or b: # OK
|
||||
if (not a) and b: # OK
|
||||
pass
|
||||
|
||||
a, b, c, d = 0, 1, 3, 0
|
||||
assert ((not a) and b) or c or d # OK
|
||||
@@ -1,12 +1,14 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
|
||||
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix;
|
||||
use crate::rules::{
|
||||
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
|
||||
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint,
|
||||
ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
|
||||
@@ -26,6 +28,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::UndefinedLocal,
|
||||
Rule::UnusedAnnotation,
|
||||
Rule::UnusedClassMethodArgument,
|
||||
Rule::BuiltinAttributeShadowing,
|
||||
Rule::UnusedFunctionArgument,
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedLambdaArgument,
|
||||
@@ -144,20 +147,17 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
checker.semantic.different_branches(left, right)
|
||||
})
|
||||
binding
|
||||
.source
|
||||
.map_or(true, |right| !checker.semantic.same_branch(left, right))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let line = checker.locator.compute_line_index(shadowed.start());
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::ImportShadowedByLoopVar {
|
||||
name: name.to_string(),
|
||||
line,
|
||||
row: checker.compute_source_row(shadowed.start()),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
@@ -236,25 +236,47 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
checker.semantic.different_branches(left, right)
|
||||
})
|
||||
binding
|
||||
.source
|
||||
.map_or(true, |right| !checker.semantic.same_branch(left, right))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let line = checker.locator.compute_line_index(shadowed.start());
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
pyflakes::rules::RedefinedWhileUnused {
|
||||
name: (*name).to_string(),
|
||||
line,
|
||||
row: checker.compute_source_row(shadowed.start()),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
if let Some(range) = binding.parent_range(&checker.semantic) {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
if let Some(import) = binding.as_any_import() {
|
||||
if let Some(source) = binding.source {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let statement = checker.semantic().statement(source);
|
||||
let parent = checker.semantic().parent_statement(source);
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once(import.member_name().as_ref()),
|
||||
statement,
|
||||
parent,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(source),
|
||||
)))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -277,6 +299,18 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
|
||||
}
|
||||
|
||||
if let Some(class_def) = scope.kind.as_class() {
|
||||
if checker.enabled(Rule::BuiltinAttributeShadowing) {
|
||||
flake8_builtins::rules::builtin_attribute_shadowing(
|
||||
checker,
|
||||
scope_id,
|
||||
scope,
|
||||
class_def,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
|
||||
|
||||
@@ -242,13 +242,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
|
||||
if checker.enabled(Rule::BuiltinAttributeShadowing) {
|
||||
flake8_builtins::rules::builtin_attribute_shadowing(
|
||||
checker, class_def, id, *range,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if !checker.semantic.current_scope().kind.is_class() {
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range);
|
||||
}
|
||||
@@ -724,7 +718,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::BooleanPositionalValueInCall) {
|
||||
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
|
||||
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::Debugger) {
|
||||
flake8_debugger::rules::debugger_call(checker, expr, func);
|
||||
@@ -869,6 +863,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::DictGetWithNoneDefault) {
|
||||
flake8_simplify::rules::dict_get_with_none_default(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::ZipDictKeysAndValues) {
|
||||
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::OsPathAbspath,
|
||||
Rule::OsChmod,
|
||||
@@ -962,6 +959,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::TrioZeroSleepCall) {
|
||||
flake8_trio::rules::zero_sleep_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryDunderCall) {
|
||||
pylint::rules::unnecessary_dunder_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::SslWithNoVersion) {
|
||||
flake8_bandit::rules::ssl_with_no_version(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::SslInsecureVersion) {
|
||||
flake8_bandit::rules::ssl_insecure_version(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
@@ -1387,12 +1393,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::reimplemented_starmap(checker, &comp.into());
|
||||
}
|
||||
}
|
||||
Expr::DictComp(ast::ExprDictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
range: _,
|
||||
}) => {
|
||||
Expr::DictComp(
|
||||
dict_comp @ ast::ExprDictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
|
||||
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
|
||||
}
|
||||
@@ -1413,7 +1421,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::StaticKeyDictComprehension) {
|
||||
ruff::rules::static_key_dict_comprehension(checker, key);
|
||||
ruff::rules::static_key_dict_comprehension(checker, dict_comp);
|
||||
}
|
||||
}
|
||||
Expr::GeneratorExp(
|
||||
@@ -1481,6 +1489,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryKeyCheck) {
|
||||
ruff::rules::unnecessary_key_check(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::ParenthesizeChainedOperators) {
|
||||
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
|
||||
}
|
||||
}
|
||||
Expr::NamedExpr(..) => {
|
||||
if checker.enabled(Rule::AssignmentInAssert) {
|
||||
|
||||
@@ -347,17 +347,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::FStringDocstring) {
|
||||
flake8_bugbear::rules::f_string_docstring(checker, body);
|
||||
}
|
||||
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
|
||||
if checker.enabled(Rule::BuiltinAttributeShadowing) {
|
||||
flake8_builtins::rules::builtin_method_shadowing(
|
||||
checker,
|
||||
class_def,
|
||||
name,
|
||||
decorator_list,
|
||||
name.range(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if !checker.semantic.current_scope().kind.is_class() {
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
|
||||
}
|
||||
@@ -374,6 +364,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &function_def.into());
|
||||
}
|
||||
if checker.enabled(Rule::SslWithBadDefaults) {
|
||||
flake8_bandit::rules::ssl_with_bad_defaults(checker, function_def);
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
if checker.enabled(Rule::ReturnOutsideFunction) {
|
||||
@@ -552,6 +545,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::DeprecatedMockImport) {
|
||||
pyupgrade::rules::deprecated_mock_import(checker, stmt);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::SuspiciousTelnetlibImport,
|
||||
Rule::SuspiciousFtplibImport,
|
||||
Rule::SuspiciousPickleImport,
|
||||
Rule::SuspiciousSubprocessImport,
|
||||
Rule::SuspiciousXmlEtreeImport,
|
||||
Rule::SuspiciousXmlSaxImport,
|
||||
Rule::SuspiciousXmlExpatImport,
|
||||
Rule::SuspiciousXmlMinidomImport,
|
||||
Rule::SuspiciousXmlPulldomImport,
|
||||
Rule::SuspiciousLxmlImport,
|
||||
Rule::SuspiciousXmlrpcImport,
|
||||
Rule::SuspiciousHttpoxyImport,
|
||||
Rule::SuspiciousPycryptoImport,
|
||||
Rule::SuspiciousPyghmiImport,
|
||||
]) {
|
||||
flake8_bandit::rules::suspicious_imports(checker, stmt);
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if checker.enabled(Rule::NonAsciiImportName) {
|
||||
@@ -751,6 +762,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pyupgrade::rules::unnecessary_builtin_import(checker, stmt, module, names);
|
||||
}
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::SuspiciousTelnetlibImport,
|
||||
Rule::SuspiciousFtplibImport,
|
||||
Rule::SuspiciousPickleImport,
|
||||
Rule::SuspiciousSubprocessImport,
|
||||
Rule::SuspiciousXmlEtreeImport,
|
||||
Rule::SuspiciousXmlSaxImport,
|
||||
Rule::SuspiciousXmlExpatImport,
|
||||
Rule::SuspiciousXmlMinidomImport,
|
||||
Rule::SuspiciousXmlPulldomImport,
|
||||
Rule::SuspiciousLxmlImport,
|
||||
Rule::SuspiciousXmlrpcImport,
|
||||
Rule::SuspiciousHttpoxyImport,
|
||||
Rule::SuspiciousPycryptoImport,
|
||||
Rule::SuspiciousPyghmiImport,
|
||||
]) {
|
||||
flake8_bandit::rules::suspicious_imports(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::BannedApi) {
|
||||
if let Some(module) =
|
||||
helpers::resolve_imported_module_path(level, module, checker.module_path)
|
||||
|
||||
@@ -37,7 +37,7 @@ use ruff_python_ast::{
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
|
||||
use ruff_python_ast::helpers::{
|
||||
collect_import_from_member, extract_handled_exceptions, to_module_path,
|
||||
@@ -56,7 +56,7 @@ use ruff_python_semantic::{
|
||||
StarImport, SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_source_file::{Locator, OneIndexed, SourceRow};
|
||||
|
||||
use crate::checkers::ast::annotation::AnnotationContext;
|
||||
use crate::checkers::ast::deferred::Deferred;
|
||||
@@ -83,6 +83,8 @@ pub(crate) struct Checker<'a> {
|
||||
pub(crate) source_type: PySourceType,
|
||||
/// The [`CellOffsets`] for the current file, if it's a Jupyter notebook.
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
/// The [`NotebookIndex`] for the current file, if it's a Jupyter notebook.
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
|
||||
/// comments).
|
||||
noqa: flags::Noqa,
|
||||
@@ -128,6 +130,7 @@ impl<'a> Checker<'a> {
|
||||
importer: Importer<'a>,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
) -> Checker<'a> {
|
||||
Checker {
|
||||
settings,
|
||||
@@ -146,6 +149,7 @@ impl<'a> Checker<'a> {
|
||||
diagnostics: Vec::default(),
|
||||
flake8_bugbear_seen: Vec::default(),
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
last_stmt_end: TextSize::default(),
|
||||
}
|
||||
}
|
||||
@@ -198,6 +202,20 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`SourceRow`] for the given offset.
|
||||
pub(crate) fn compute_source_row(&self, offset: TextSize) -> SourceRow {
|
||||
#[allow(deprecated)]
|
||||
let line = self.locator.compute_line_index(offset);
|
||||
|
||||
if let Some(notebook_index) = self.notebook_index {
|
||||
let cell = notebook_index.cell(line).unwrap_or(OneIndexed::MIN);
|
||||
let line = notebook_index.cell_row(line).unwrap_or(OneIndexed::MIN);
|
||||
SourceRow::Notebook { cell, line }
|
||||
} else {
|
||||
SourceRow::SourceFile { line }
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Locator`] for the current file, which enables extraction of source code from byte
|
||||
/// offsets.
|
||||
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
|
||||
@@ -1984,6 +2002,7 @@ pub(crate) fn check_ast(
|
||||
package: Option<&Path>,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let module_path = package.and_then(|package| to_module_path(package, path));
|
||||
let module = Module {
|
||||
@@ -2013,6 +2032,7 @@ pub(crate) fn check_ast(
|
||||
Importer::new(python_ast, locator, stylist),
|
||||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
);
|
||||
checker.bind_builtins();
|
||||
|
||||
|
||||
@@ -214,6 +214,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
|
||||
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
|
||||
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
|
||||
(Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
|
||||
@@ -471,6 +472,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
|
||||
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
|
||||
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
|
||||
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
|
||||
|
||||
// flake8-copyright
|
||||
#[allow(deprecated)]
|
||||
@@ -626,7 +628,24 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "321") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousFTPLibUsage),
|
||||
(Flake8Bandit, "323") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage),
|
||||
(Flake8Bandit, "324") => (RuleGroup::Stable, rules::flake8_bandit::rules::HashlibInsecureHashFunction),
|
||||
(Flake8Bandit, "401") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousTelnetlibImport),
|
||||
(Flake8Bandit, "402") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousFtplibImport),
|
||||
(Flake8Bandit, "403") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPickleImport),
|
||||
(Flake8Bandit, "404") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousSubprocessImport),
|
||||
(Flake8Bandit, "405") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlEtreeImport),
|
||||
(Flake8Bandit, "406") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlSaxImport),
|
||||
(Flake8Bandit, "407") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlExpatImport),
|
||||
(Flake8Bandit, "408") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlMinidomImport),
|
||||
(Flake8Bandit, "409") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlPulldomImport),
|
||||
(Flake8Bandit, "410") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousLxmlImport),
|
||||
(Flake8Bandit, "411") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlrpcImport),
|
||||
(Flake8Bandit, "412") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousHttpoxyImport),
|
||||
(Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
|
||||
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
|
||||
(Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation),
|
||||
(Flake8Bandit, "502") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslInsecureVersion),
|
||||
(Flake8Bandit, "503") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithBadDefaults),
|
||||
(Flake8Bandit, "504") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithNoVersion),
|
||||
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
|
||||
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
|
||||
(Flake8Bandit, "507") => (RuleGroup::Preview, rules::flake8_bandit::rules::SSHNoHostKeyVerification),
|
||||
@@ -905,6 +924,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
|
||||
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
|
||||
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
|
||||
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn extract_directives(
|
||||
NoqaMapping::default()
|
||||
},
|
||||
isort: if flags.intersects(Flags::ISORT) {
|
||||
extract_isort_directives(lxr, locator)
|
||||
extract_isort_directives(locator, indexer)
|
||||
} else {
|
||||
IsortDirectives::default()
|
||||
},
|
||||
@@ -215,15 +215,13 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
|
||||
}
|
||||
|
||||
/// Extract a set of ranges over which to disable isort.
|
||||
fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
|
||||
fn extract_isort_directives(locator: &Locator, indexer: &Indexer) -> IsortDirectives {
|
||||
let mut exclusions: Vec<TextRange> = Vec::default();
|
||||
let mut splits: Vec<TextSize> = Vec::default();
|
||||
let mut off: Option<TextSize> = None;
|
||||
|
||||
for &(ref tok, range) in lxr.iter().flatten() {
|
||||
let Tok::Comment(comment_text) = tok else {
|
||||
continue;
|
||||
};
|
||||
for range in indexer.comment_ranges() {
|
||||
let comment_text = locator.slice(range);
|
||||
|
||||
// `isort` allows for `# isort: skip` and `# isort: skip_file` to include or
|
||||
// omit a space after the colon. The remaining action comments are
|
||||
@@ -592,8 +590,10 @@ assert foo, \
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
|
||||
@@ -603,8 +603,10 @@ y = 2
|
||||
# isort: on
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(25))])
|
||||
);
|
||||
|
||||
@@ -616,8 +618,10 @@ y = 2
|
||||
z = x + 1
|
||||
# isort: on";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(38))])
|
||||
);
|
||||
|
||||
@@ -626,8 +630,10 @@ x = 1
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::from_iter([TextRange::at(TextSize::from(0), contents.text_len())])
|
||||
);
|
||||
|
||||
@@ -636,8 +642,10 @@ x = 1
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
|
||||
@@ -648,8 +656,10 @@ y = 2
|
||||
# isort: skip_file
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
extract_isort_directives(&locator, &indexer).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
}
|
||||
@@ -660,8 +670,10 @@ z = x + 1";
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
extract_isort_directives(&locator, &indexer).splits,
|
||||
Vec::new()
|
||||
);
|
||||
|
||||
@@ -670,8 +682,10 @@ y = 2
|
||||
# isort: split
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
extract_isort_directives(&locator, &indexer).splits,
|
||||
vec![TextSize::from(12)]
|
||||
);
|
||||
|
||||
@@ -679,8 +693,10 @@ z = x + 1";
|
||||
y = 2 # isort: split
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
extract_isort_directives(&locator, &indexer).splits,
|
||||
vec![TextSize::from(13)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,13 +153,17 @@ impl<'a> SectionContexts<'a> {
|
||||
while let Some(line) = lines.next() {
|
||||
if let Some(section_kind) = suspected_as_section(&line, style) {
|
||||
let indent = leading_space(&line);
|
||||
let section_name = leading_words(&line);
|
||||
let indent_size = indent.text_len();
|
||||
|
||||
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
|
||||
let section_name = leading_words(&line);
|
||||
let section_name_size = section_name.text_len();
|
||||
|
||||
if is_docstring_section(
|
||||
&line,
|
||||
section_name_range,
|
||||
indent_size,
|
||||
section_name_size,
|
||||
section_kind,
|
||||
last.as_ref(),
|
||||
previous_line.as_ref(),
|
||||
lines.peek(),
|
||||
) {
|
||||
@@ -170,7 +174,8 @@ impl<'a> SectionContexts<'a> {
|
||||
|
||||
last = Some(SectionContextData {
|
||||
kind: section_kind,
|
||||
name_range: section_name_range + line.start(),
|
||||
indent_size: indent.text_len(),
|
||||
name_range: TextRange::at(line.start() + indent_size, section_name_size),
|
||||
range: TextRange::empty(line.start()),
|
||||
summary_full_end: line.full_end(),
|
||||
});
|
||||
@@ -204,8 +209,8 @@ impl<'a> SectionContexts<'a> {
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a SectionContexts<'a> {
|
||||
type IntoIter = SectionContextsIter<'a>;
|
||||
type Item = SectionContext<'a>;
|
||||
type IntoIter = SectionContextsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
@@ -257,6 +262,9 @@ impl ExactSizeIterator for SectionContextsIter<'_> {}
|
||||
struct SectionContextData {
|
||||
kind: SectionKind,
|
||||
|
||||
/// The size of the indentation of the section name.
|
||||
indent_size: TextSize,
|
||||
|
||||
/// Range of the section name, relative to the [`Docstring::body`]
|
||||
name_range: TextRange,
|
||||
|
||||
@@ -401,12 +409,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
|
||||
/// Check if the suspected context is really a section header.
|
||||
fn is_docstring_section(
|
||||
line: &Line,
|
||||
section_name_range: TextRange,
|
||||
indent_size: TextSize,
|
||||
section_name_size: TextSize,
|
||||
section_kind: SectionKind,
|
||||
previous_section: Option<&SectionContextData>,
|
||||
previous_line: Option<&Line>,
|
||||
next_line: Option<&Line>,
|
||||
) -> bool {
|
||||
// Determine whether the current line looks like a section header, e.g., "Args:".
|
||||
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
|
||||
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
|
||||
let this_looks_like_a_section_name =
|
||||
section_name_suffix == ":" || section_name_suffix.is_empty();
|
||||
if !this_looks_like_a_section_name {
|
||||
@@ -439,5 +450,25 @@ fn is_docstring_section(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine if this is a sub-section within another section, like `args` in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
|
||||
// continue to treat it as a section header.
|
||||
if let Some(previous_section) = previous_section {
|
||||
if previous_section.indent_size < indent_size {
|
||||
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ pub fn check_path(
|
||||
match tokens.into_ast_source(source_kind, source_type) {
|
||||
Ok(python_ast) => {
|
||||
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
|
||||
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
|
||||
if use_ast {
|
||||
diagnostics.extend(check_ast(
|
||||
&python_ast,
|
||||
@@ -161,6 +162,7 @@ pub fn check_path(
|
||||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
));
|
||||
}
|
||||
if use_imports {
|
||||
|
||||
@@ -216,12 +216,7 @@ impl Display for DisplayParseError {
|
||||
colon = ":".cyan(),
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{header}{colon}",
|
||||
header = "Failed to parse".bold(),
|
||||
colon = ":".cyan(),
|
||||
)?;
|
||||
write!(f, "{header}", header = "Failed to parse at ".bold())?;
|
||||
}
|
||||
match &self.location {
|
||||
ErrorLocation::File(location) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_macros::CacheKey;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
const RULESET_SIZE: usize = 12;
|
||||
const RULESET_SIZE: usize = 13;
|
||||
|
||||
/// A set of [`Rule`]s.
|
||||
///
|
||||
|
||||
@@ -89,7 +89,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
// if x > 0:
|
||||
// return 1
|
||||
// ```
|
||||
if terminal == Terminal::ConditionalReturn || terminal == Terminal::None {
|
||||
if terminal.has_implicit_return() {
|
||||
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
|
||||
}
|
||||
|
||||
|
||||
@@ -263,14 +263,14 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
|
||||
83 | match x:
|
||||
84 | case [1, 2, 3]:
|
||||
|
|
||||
= help: Add return type annotation: `str | int`
|
||||
= help: Add return type annotation: `str | int | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
79 79 | return 1
|
||||
80 80 |
|
||||
81 81 |
|
||||
82 |-def func(x: int):
|
||||
82 |+def func(x: int) -> str | int:
|
||||
82 |+def func(x: int) -> str | int | None:
|
||||
83 83 | match x:
|
||||
84 84 | case [1, 2, 3]:
|
||||
85 85 | return 1
|
||||
@@ -396,14 +396,14 @@ auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public
|
||||
138 | try:
|
||||
139 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
= help: Add return type annotation: `int | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
134 134 | return 2
|
||||
135 135 |
|
||||
136 136 |
|
||||
137 |-def func(x: int):
|
||||
137 |+def func(x: int) -> int:
|
||||
137 |+def func(x: int) -> int | None:
|
||||
138 138 | try:
|
||||
139 139 | return 1
|
||||
140 140 | except:
|
||||
@@ -674,4 +674,99 @@ auto_return_type.py:262:5: ANN201 [*] Missing return type annotation for public
|
||||
264 264 | if x > 0:
|
||||
265 265 | return 1
|
||||
|
||||
auto_return_type.py:269:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
269 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
270 | if x > 5:
|
||||
271 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
266 266 | raise ValueError
|
||||
267 267 |
|
||||
268 268 |
|
||||
269 |-def func(x: int):
|
||||
269 |+def func(x: int) -> None:
|
||||
270 270 | if x > 5:
|
||||
271 271 | raise ValueError
|
||||
272 272 | else:
|
||||
|
||||
auto_return_type.py:276:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
276 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
277 | if x > 5:
|
||||
278 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
273 273 | pass
|
||||
274 274 |
|
||||
275 275 |
|
||||
276 |-def func(x: int):
|
||||
276 |+def func(x: int) -> None:
|
||||
277 277 | if x > 5:
|
||||
278 278 | raise ValueError
|
||||
279 279 | elif x > 10:
|
||||
|
||||
auto_return_type.py:283:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
283 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
284 | if x > 5:
|
||||
285 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `int | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
280 280 | pass
|
||||
281 281 |
|
||||
282 282 |
|
||||
283 |-def func(x: int):
|
||||
283 |+def func(x: int) -> int | None:
|
||||
284 284 | if x > 5:
|
||||
285 285 | raise ValueError
|
||||
286 286 | elif x > 10:
|
||||
|
||||
auto_return_type.py:290:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
290 | def func():
|
||||
| ^^^^ ANN201
|
||||
291 | try:
|
||||
292 | return 5
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
287 287 | return 5
|
||||
288 288 |
|
||||
289 289 |
|
||||
290 |-def func():
|
||||
290 |+def func() -> int:
|
||||
291 291 | try:
|
||||
292 292 | return 5
|
||||
293 293 | except:
|
||||
|
||||
auto_return_type.py:299:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
299 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
300 | match x:
|
||||
301 | case [1, 2, 3]:
|
||||
|
|
||||
= help: Add return type annotation: `str | int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
296 296 | raise ValueError
|
||||
297 297 |
|
||||
298 298 |
|
||||
299 |-def func(x: int):
|
||||
299 |+def func(x: int) -> str | int:
|
||||
300 300 | match x:
|
||||
301 301 | case [1, 2, 3]:
|
||||
302 302 | return 1
|
||||
|
||||
|
||||
|
||||
@@ -293,7 +293,7 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
|
||||
83 | match x:
|
||||
84 | case [1, 2, 3]:
|
||||
|
|
||||
= help: Add return type annotation: `Union[str | int]`
|
||||
= help: Add return type annotation: `Union[str | int | None]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import Union
|
||||
@@ -305,7 +305,7 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
|
||||
80 81 |
|
||||
81 82 |
|
||||
82 |-def func(x: int):
|
||||
83 |+def func(x: int) -> Union[str | int]:
|
||||
83 |+def func(x: int) -> Union[str | int | None]:
|
||||
83 84 | match x:
|
||||
84 85 | case [1, 2, 3]:
|
||||
85 86 | return 1
|
||||
@@ -446,17 +446,22 @@ auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public
|
||||
138 | try:
|
||||
139 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
= help: Add return type annotation: `Optional[int]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
134 134 | return 2
|
||||
135 135 |
|
||||
136 136 |
|
||||
1 |+from typing import Optional
|
||||
1 2 | def func():
|
||||
2 3 | return 1
|
||||
3 4 |
|
||||
--------------------------------------------------------------------------------
|
||||
134 135 | return 2
|
||||
135 136 |
|
||||
136 137 |
|
||||
137 |-def func(x: int):
|
||||
137 |+def func(x: int) -> int:
|
||||
138 138 | try:
|
||||
139 139 | return 1
|
||||
140 140 | except:
|
||||
138 |+def func(x: int) -> Optional[int]:
|
||||
138 139 | try:
|
||||
139 140 | return 1
|
||||
140 141 | except:
|
||||
|
||||
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
@@ -755,4 +760,117 @@ auto_return_type.py:262:5: ANN201 [*] Missing return type annotation for public
|
||||
264 264 | if x > 0:
|
||||
265 265 | return 1
|
||||
|
||||
auto_return_type.py:269:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
269 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
270 | if x > 5:
|
||||
271 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
266 266 | raise ValueError
|
||||
267 267 |
|
||||
268 268 |
|
||||
269 |-def func(x: int):
|
||||
269 |+def func(x: int) -> None:
|
||||
270 270 | if x > 5:
|
||||
271 271 | raise ValueError
|
||||
272 272 | else:
|
||||
|
||||
auto_return_type.py:276:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
276 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
277 | if x > 5:
|
||||
278 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
273 273 | pass
|
||||
274 274 |
|
||||
275 275 |
|
||||
276 |-def func(x: int):
|
||||
276 |+def func(x: int) -> None:
|
||||
277 277 | if x > 5:
|
||||
278 278 | raise ValueError
|
||||
279 279 | elif x > 10:
|
||||
|
||||
auto_return_type.py:283:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
283 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
284 | if x > 5:
|
||||
285 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `Optional[int]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
214 214 | return 1
|
||||
215 215 |
|
||||
216 216 |
|
||||
217 |-from typing import overload
|
||||
217 |+from typing import overload, Optional
|
||||
218 218 |
|
||||
219 219 |
|
||||
220 220 | @overload
|
||||
--------------------------------------------------------------------------------
|
||||
280 280 | pass
|
||||
281 281 |
|
||||
282 282 |
|
||||
283 |-def func(x: int):
|
||||
283 |+def func(x: int) -> Optional[int]:
|
||||
284 284 | if x > 5:
|
||||
285 285 | raise ValueError
|
||||
286 286 | elif x > 10:
|
||||
|
||||
auto_return_type.py:290:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
290 | def func():
|
||||
| ^^^^ ANN201
|
||||
291 | try:
|
||||
292 | return 5
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
287 287 | return 5
|
||||
288 288 |
|
||||
289 289 |
|
||||
290 |-def func():
|
||||
290 |+def func() -> int:
|
||||
291 291 | try:
|
||||
292 292 | return 5
|
||||
293 293 | except:
|
||||
|
||||
auto_return_type.py:299:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
299 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
300 | match x:
|
||||
301 | case [1, 2, 3]:
|
||||
|
|
||||
= help: Add return type annotation: `Union[str | int]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
214 214 | return 1
|
||||
215 215 |
|
||||
216 216 |
|
||||
217 |-from typing import overload
|
||||
217 |+from typing import overload, Union
|
||||
218 218 |
|
||||
219 219 |
|
||||
220 220 | @overload
|
||||
--------------------------------------------------------------------------------
|
||||
296 296 | raise ValueError
|
||||
297 297 |
|
||||
298 298 |
|
||||
299 |-def func(x: int):
|
||||
299 |+def func(x: int) -> Union[str | int]:
|
||||
300 300 | match x:
|
||||
301 301 | case [1, 2, 3]:
|
||||
302 302 | return 1
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ mod tests {
|
||||
#[test_case(Rule::SSHNoHostKeyVerification, Path::new("S507.py"))]
|
||||
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
|
||||
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
|
||||
#[test_case(Rule::SslInsecureVersion, Path::new("S502.py"))]
|
||||
#[test_case(Rule::SslWithBadDefaults, Path::new("S503.py"))]
|
||||
#[test_case(Rule::SslWithNoVersion, Path::new("S504.py"))]
|
||||
#[test_case(Rule::StartProcessWithAShell, Path::new("S605.py"))]
|
||||
#[test_case(Rule::StartProcessWithNoShell, Path::new("S606.py"))]
|
||||
#[test_case(Rule::StartProcessWithPartialPath, Path::new("S607.py"))]
|
||||
@@ -45,6 +48,20 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
|
||||
#[test_case(Rule::SuspiciousFtplibImport, Path::new("S402.py"))]
|
||||
#[test_case(Rule::SuspiciousPickleImport, Path::new("S403.py"))]
|
||||
#[test_case(Rule::SuspiciousSubprocessImport, Path::new("S404.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlEtreeImport, Path::new("S405.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlSaxImport, Path::new("S406.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlExpatImport, Path::new("S407.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.py"))]
|
||||
#[test_case(Rule::SuspiciousLxmlImport, Path::new("S410.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlrpcImport, Path::new("S411.py"))]
|
||||
#[test_case(Rule::SuspiciousHttpoxyImport, Path::new("S412.py"))]
|
||||
#[test_case(Rule::SuspiciousPycryptoImport, Path::new("S413.py"))]
|
||||
#[test_case(Rule::SuspiciousPyghmiImport, Path::new("S415.py"))]
|
||||
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
|
||||
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]
|
||||
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]
|
||||
|
||||
@@ -20,7 +20,11 @@ pub(crate) use shell_injection::*;
|
||||
pub(crate) use snmp_insecure_version::*;
|
||||
pub(crate) use snmp_weak_cryptography::*;
|
||||
pub(crate) use ssh_no_host_key_verification::*;
|
||||
pub(crate) use ssl_insecure_version::*;
|
||||
pub(crate) use ssl_with_bad_defaults::*;
|
||||
pub(crate) use ssl_with_no_version::*;
|
||||
pub(crate) use suspicious_function_call::*;
|
||||
pub(crate) use suspicious_imports::*;
|
||||
pub(crate) use tarfile_unsafe_members::*;
|
||||
pub(crate) use try_except_continue::*;
|
||||
pub(crate) use try_except_pass::*;
|
||||
@@ -49,7 +53,11 @@ mod shell_injection;
|
||||
mod snmp_insecure_version;
|
||||
mod snmp_weak_cryptography;
|
||||
mod ssh_no_host_key_verification;
|
||||
mod ssl_insecure_version;
|
||||
mod ssl_with_bad_defaults;
|
||||
mod ssl_with_no_version;
|
||||
mod suspicious_function_call;
|
||||
mod suspicious_imports;
|
||||
mod tarfile_unsafe_members;
|
||||
mod try_except_continue;
|
||||
mod try_except_pass;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for function calls with parameters that indicate the use of insecure
|
||||
/// SSL and TLS protocol versions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Several highly publicized exploitable flaws have been discovered in all
|
||||
/// versions of SSL and early versions of TLS. The following versions are
|
||||
/// considered insecure, and should be avoided:
|
||||
/// - SSL v2
|
||||
/// - SSL v3
|
||||
/// - TLS v1
|
||||
/// - TLS v1.1
|
||||
///
|
||||
/// This method supports detection on the Python's built-in `ssl` module and
|
||||
/// the `pyOpenSSL` module.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2)
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SslInsecureVersion {
|
||||
protocol: String,
|
||||
}
|
||||
|
||||
impl Violation for SslInsecureVersion {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SslInsecureVersion { protocol } = self;
|
||||
format!("Call made with insecure SSL protocol: `{protocol}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// S502
|
||||
pub(crate) fn ssl_insecure_version(checker: &mut Checker, call: &ExprCall) {
|
||||
let Some(keyword) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.and_then(|call_path| match call_path.as_slice() {
|
||||
["ssl", "wrap_socket"] => Some("ssl_version"),
|
||||
["OpenSSL", "SSL", "Context"] => Some("method"),
|
||||
_ => None,
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(keyword) = call.arguments.find_keyword(keyword) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match &keyword.value {
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
if is_insecure_protocol(id) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SslInsecureVersion {
|
||||
protocol: id.to_string(),
|
||||
},
|
||||
keyword.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
|
||||
if is_insecure_protocol(attr) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SslInsecureVersion {
|
||||
protocol: attr.to_string(),
|
||||
},
|
||||
keyword.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given protocol name is insecure.
|
||||
fn is_insecure_protocol(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"PROTOCOL_SSLv2"
|
||||
| "PROTOCOL_SSLv3"
|
||||
| "PROTOCOL_TLSv1"
|
||||
| "PROTOCOL_TLSv1_1"
|
||||
| "SSLv2_METHOD"
|
||||
| "SSLv23_METHOD"
|
||||
| "SSLv3_METHOD"
|
||||
| "TLSv1_METHOD"
|
||||
| "TLSv1_1_METHOD"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, StmtFunctionDef};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for function definitions with default arguments set to insecure SSL
|
||||
/// and TLS protocol versions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Several highly publicized exploitable flaws have been discovered in all
|
||||
/// versions of SSL and early versions of TLS. The following versions are
|
||||
/// considered insecure, and should be avoided:
|
||||
/// - SSL v2
|
||||
/// - SSL v3
|
||||
/// - TLS v1
|
||||
/// - TLS v1.1
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
///
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
///
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1_2):
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SslWithBadDefaults {
|
||||
protocol: String,
|
||||
}
|
||||
|
||||
impl Violation for SslWithBadDefaults {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SslWithBadDefaults { protocol } = self;
|
||||
format!("Argument default set to insecure SSL protocol: `{protocol}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// S503
|
||||
pub(crate) fn ssl_with_bad_defaults(checker: &mut Checker, function_def: &StmtFunctionDef) {
|
||||
function_def
|
||||
.parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(
|
||||
function_def
|
||||
.parameters
|
||||
.args
|
||||
.iter()
|
||||
.chain(function_def.parameters.kwonlyargs.iter()),
|
||||
)
|
||||
.for_each(|param| {
|
||||
if let Some(default) = ¶m.default {
|
||||
match default.as_ref() {
|
||||
Expr::Name(ast::ExprName { id, range, .. }) => {
|
||||
if is_insecure_protocol(id.as_str()) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SslWithBadDefaults {
|
||||
protocol: id.to_string(),
|
||||
},
|
||||
*range,
|
||||
));
|
||||
}
|
||||
}
|
||||
Expr::Attribute(ast::ExprAttribute { attr, range, .. }) => {
|
||||
if is_insecure_protocol(attr.as_str()) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SslWithBadDefaults {
|
||||
protocol: attr.to_string(),
|
||||
},
|
||||
*range,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns `true` if the given protocol name is insecure.
|
||||
fn is_insecure_protocol(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"PROTOCOL_SSLv2"
|
||||
| "PROTOCOL_SSLv3"
|
||||
| "PROTOCOL_TLSv1"
|
||||
| "PROTOCOL_TLSv1_1"
|
||||
| "SSLv2_METHOD"
|
||||
| "SSLv23_METHOD"
|
||||
| "SSLv3_METHOD"
|
||||
| "TLSv1_METHOD"
|
||||
| "TLSv1_1_METHOD"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::ExprCall;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for calls to `ssl.wrap_socket()` without an `ssl_version`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// This method is known to provide a default value that maximizes
|
||||
/// compatibility, but permits use of insecure protocols.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
/// ssl.wrap_socket()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2)
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SslWithNoVersion;
|
||||
|
||||
impl Violation for SslWithNoVersion {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`ssl.wrap_socket` called without an `ssl_version``")
|
||||
}
|
||||
}
|
||||
|
||||
/// S504
|
||||
pub(crate) fn ssl_with_no_version(checker: &mut Checker, call: &ExprCall) {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ssl", "wrap_socket"]))
|
||||
{
|
||||
if call.arguments.find_keyword("ssl_version").is_none() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SslWithNoVersion, call.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
//! Check for imports of or from suspicious modules.
|
||||
//!
|
||||
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html>
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the`telnetlib` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Telnet is considered insecure. Instead, use SSH or another encrypted
|
||||
/// protocol.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import telnetlib
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousTelnetlibImport;
|
||||
|
||||
impl Violation for SuspiciousTelnetlibImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `ftplib` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// FTP is considered insecure. Instead, use SSH, SFTP, SCP, or another
|
||||
/// encrypted protocol.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import ftplib
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousFtplibImport;
|
||||
|
||||
impl Violation for SuspiciousFtplibImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `pickle`, `cPickle`, `dill`, and `shelve` modules.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It is possible to construct malicious pickle data which will execute
|
||||
/// arbitrary code during unpickling. Consider possible security implications
|
||||
/// associated with these modules.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pickle
|
||||
/// ```
|
||||
/// /// ## References
|
||||
/// - [Python Docs](https://docs.python.org/3/library/pickle.html)
|
||||
#[violation]
|
||||
pub struct SuspiciousPickleImport;
|
||||
|
||||
impl Violation for SuspiciousPickleImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `subprocess` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It is possible to inject malicious commands into subprocess calls. Consider
|
||||
/// possible security implications associated with this module.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import subprocess
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousSubprocessImport;
|
||||
|
||||
impl Violation for SuspiciousSubprocessImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`subprocess` module is possibly insecure")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xml.etree.cElementTree` and `xml.etree.ElementTree` modules
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from these modules to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
|
||||
/// called before parsing XML data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xml.etree.cElementTree
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlEtreeImport;
|
||||
|
||||
impl Violation for SuspiciousXmlEtreeImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`xml.etree` methods are vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xml.sax` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from these modules to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
|
||||
/// called before parsing XML data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xml.sax
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlSaxImport;
|
||||
|
||||
impl Violation for SuspiciousXmlSaxImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`xml.sax` methods are vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xml.dom.expatbuilder` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from these modules to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
|
||||
/// called before parsing XML data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xml.dom.expatbuilder
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlExpatImport;
|
||||
|
||||
impl Violation for SuspiciousXmlExpatImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`xml.dom.expatbuilder` is vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xml.dom.minidom` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from these modules to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
|
||||
/// called before parsing XML data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xml.dom.minidom
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlMinidomImport;
|
||||
|
||||
impl Violation for SuspiciousXmlMinidomImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`xml.dom.minidom` is vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xml.dom.pulldom` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from these modules to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
|
||||
/// called before parsing XML data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xml.dom.pulldom
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlPulldomImport;
|
||||
|
||||
impl Violation for SuspiciousXmlPulldomImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`xml.dom.pulldom` is vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the`lxml` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using various methods from the `lxml` module to parse untrusted XML data is
|
||||
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
|
||||
/// equivalent `defusedxml` package.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import lxml
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousLxmlImport;
|
||||
|
||||
impl Violation for SuspiciousLxmlImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`lxml` is vulnerable to XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `xmlrpc` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// XMLRPC is a particularly dangerous XML module as it is also concerned with
|
||||
/// communicating data over a network. Use the `defused.xmlrpc.monkey_patch()`
|
||||
/// function to monkey-patch the `xmlrpclib` module and mitigate remote XML
|
||||
/// attacks.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import xmlrpc
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SuspiciousXmlrpcImport;
|
||||
|
||||
impl Violation for SuspiciousXmlrpcImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("XMLRPC is vulnerable to remote XML attacks")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of `wsgiref.handlers.CGIHandler` and
|
||||
/// `twisted.web.twcgi.CGIScript`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// httpoxy is a set of vulnerabilities that affect application code running in
|
||||
/// CGI or CGI-like environments. The use of CGI for web applications should be
|
||||
/// avoided to prevent this class of attack.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import wsgiref.handlers.CGIHandler
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [httpoxy website](https://httpoxy.org/)
|
||||
#[violation]
|
||||
pub struct SuspiciousHttpoxyImport;
|
||||
|
||||
impl Violation for SuspiciousHttpoxyImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of several unsafe cryptography modules.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `pycrypto` library is known to have a publicly disclosed buffer
|
||||
/// overflow vulnerability. It is no longer actively maintained and has been
|
||||
/// deprecated in favor of the `pyca/cryptography` library.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import Crypto.Random
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176)
|
||||
#[violation]
|
||||
pub struct SuspiciousPycryptoImport;
|
||||
|
||||
impl Violation for SuspiciousPycryptoImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"`pycrypto` library is known to have publicly disclosed buffer overflow vulnerability"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the `pyghmi` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pyghmi` is an IPMI-related module, but IPMI is considered insecure.
|
||||
/// Instead, use an encrypted protocol.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pyghmi
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176)
|
||||
#[violation]
|
||||
pub struct SuspiciousPyghmiImport;
|
||||
|
||||
impl Violation for SuspiciousPyghmiImport {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.")
|
||||
}
|
||||
}
|
||||
|
||||
/// S401, S402, S403, S404, S405, S406, S407, S408, S409, S410, S411, S412, S413, S415
|
||||
pub(crate) fn suspicious_imports(checker: &mut Checker, stmt: &Stmt) {
|
||||
match stmt {
|
||||
Stmt::Import(ast::StmtImport { names, .. }) => {
|
||||
for name in names {
|
||||
match name.name.as_str() {
|
||||
"telnetlib" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousTelnetlibImport),
|
||||
name.range,
|
||||
),
|
||||
"ftplib" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousFtplibImport),
|
||||
name.range,
|
||||
),
|
||||
"pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPickleImport),
|
||||
name.range,
|
||||
),
|
||||
"subprocess" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousSubprocessImport),
|
||||
name.range,
|
||||
),
|
||||
"xml.etree.cElementTree" | "xml.etree.ElementTree" => {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlEtreeImport),
|
||||
name.range,
|
||||
);
|
||||
}
|
||||
"xml.sax" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlSaxImport),
|
||||
name.range,
|
||||
),
|
||||
"xml.dom.expatbuilder" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlExpatImport),
|
||||
name.range,
|
||||
),
|
||||
"xml.dom.minidom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlMinidomImport),
|
||||
name.range,
|
||||
),
|
||||
"xml.dom.pulldom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlPulldomImport),
|
||||
name.range,
|
||||
),
|
||||
"lxml" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousLxmlImport),
|
||||
name.range,
|
||||
),
|
||||
"xmlrpc" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlrpcImport),
|
||||
name.range,
|
||||
),
|
||||
"Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol"
|
||||
| "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPycryptoImport),
|
||||
name.range,
|
||||
);
|
||||
}
|
||||
"pyghmi" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPyghmiImport),
|
||||
name.range,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
|
||||
let Some(identifier) = module else { return };
|
||||
match identifier.as_str() {
|
||||
"telnetlib" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousTelnetlibImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"ftplib" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousFtplibImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPickleImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"subprocess" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousSubprocessImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"xml.etree" => {
|
||||
for name in names {
|
||||
if matches!(name.name.as_str(), "cElementTree" | "ElementTree") {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlEtreeImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"xml.etree.cElementTree" | "xml.etree.ElementTree" => {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlEtreeImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
"xml" => {
|
||||
for name in names {
|
||||
if name.name.as_str() == "sax" {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlSaxImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"xml.sax" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlSaxImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"xml.dom" => {
|
||||
for name in names {
|
||||
match name.name.as_str() {
|
||||
"expatbuilder" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlExpatImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"minidom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlMinidomImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"pulldom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlPulldomImport),
|
||||
identifier.range(),
|
||||
),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
"xml.dom.expatbuilder" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlExpatImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"xml.dom.minidom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlMinidomImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"xml.dom.pulldom" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlPulldomImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"lxml" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousLxmlImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"xmlrpc" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousXmlrpcImport),
|
||||
identifier.range(),
|
||||
),
|
||||
"wsgiref.handlers" => {
|
||||
for name in names {
|
||||
if name.name.as_str() == "CGIHandler" {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousHttpoxyImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"twisted.web.twcgi" => {
|
||||
for name in names {
|
||||
if name.name.as_str() == "CGIScript" {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousHttpoxyImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"Crypto" => {
|
||||
for name in names {
|
||||
if matches!(
|
||||
name.name.as_str(),
|
||||
"Cipher"
|
||||
| "Hash"
|
||||
| "IO"
|
||||
| "Protocol"
|
||||
| "PublicKey"
|
||||
| "Random"
|
||||
| "Signature"
|
||||
| "Util"
|
||||
) {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPycryptoImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol"
|
||||
| "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => {
|
||||
check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPycryptoImport),
|
||||
identifier.range(),
|
||||
);
|
||||
}
|
||||
"pyghmi" => check_and_push_diagnostic(
|
||||
checker,
|
||||
DiagnosticKind::from(SuspiciousPyghmiImport),
|
||||
identifier.range(),
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Stmt::Import | Stmt::ImportFrom"),
|
||||
};
|
||||
}
|
||||
|
||||
fn check_and_push_diagnostic(
|
||||
checker: &mut Checker,
|
||||
diagnostic_kind: DiagnosticKind,
|
||||
range: TextRange,
|
||||
) {
|
||||
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, range);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S401.py:1:8: S401 `telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.
|
||||
|
|
||||
1 | import telnetlib # S401
|
||||
| ^^^^^^^^^ S401
|
||||
2 | from telnetlib import Telnet # S401
|
||||
|
|
||||
|
||||
S401.py:2:6: S401 `telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.
|
||||
|
|
||||
1 | import telnetlib # S401
|
||||
2 | from telnetlib import Telnet # S401
|
||||
| ^^^^^^^^^ S401
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S402.py:1:8: S402 `ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.
|
||||
|
|
||||
1 | import ftplib # S402
|
||||
| ^^^^^^ S402
|
||||
2 | from ftplib import FTP # S402
|
||||
|
|
||||
|
||||
S402.py:2:6: S402 `ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.
|
||||
|
|
||||
1 | import ftplib # S402
|
||||
2 | from ftplib import FTP # S402
|
||||
| ^^^^^^ S402
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S403.py:1:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
1 | import dill # S403
|
||||
| ^^^^ S403
|
||||
2 | from dill import objects # S403
|
||||
3 | import shelve
|
||||
|
|
||||
|
||||
S403.py:2:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
1 | import dill # S403
|
||||
2 | from dill import objects # S403
|
||||
| ^^^^ S403
|
||||
3 | import shelve
|
||||
4 | from shelve import open
|
||||
|
|
||||
|
||||
S403.py:3:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
1 | import dill # S403
|
||||
2 | from dill import objects # S403
|
||||
3 | import shelve
|
||||
| ^^^^^^ S403
|
||||
4 | from shelve import open
|
||||
5 | import cPickle
|
||||
|
|
||||
|
||||
S403.py:4:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
2 | from dill import objects # S403
|
||||
3 | import shelve
|
||||
4 | from shelve import open
|
||||
| ^^^^^^ S403
|
||||
5 | import cPickle
|
||||
6 | from cPickle import load
|
||||
|
|
||||
|
||||
S403.py:5:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
3 | import shelve
|
||||
4 | from shelve import open
|
||||
5 | import cPickle
|
||||
| ^^^^^^^ S403
|
||||
6 | from cPickle import load
|
||||
7 | import pickle
|
||||
|
|
||||
|
||||
S403.py:6:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
4 | from shelve import open
|
||||
5 | import cPickle
|
||||
6 | from cPickle import load
|
||||
| ^^^^^^^ S403
|
||||
7 | import pickle
|
||||
8 | from pickle import load
|
||||
|
|
||||
|
||||
S403.py:7:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
5 | import cPickle
|
||||
6 | from cPickle import load
|
||||
7 | import pickle
|
||||
| ^^^^^^ S403
|
||||
8 | from pickle import load
|
||||
|
|
||||
|
||||
S403.py:8:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
||||
|
|
||||
6 | from cPickle import load
|
||||
7 | import pickle
|
||||
8 | from pickle import load
|
||||
| ^^^^^^ S403
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S404.py:1:8: S404 `subprocess` module is possibly insecure
|
||||
|
|
||||
1 | import subprocess # S404
|
||||
| ^^^^^^^^^^ S404
|
||||
2 | from subprocess import Popen # S404
|
||||
3 | from subprocess import Popen as pop # S404
|
||||
|
|
||||
|
||||
S404.py:2:6: S404 `subprocess` module is possibly insecure
|
||||
|
|
||||
1 | import subprocess # S404
|
||||
2 | from subprocess import Popen # S404
|
||||
| ^^^^^^^^^^ S404
|
||||
3 | from subprocess import Popen as pop # S404
|
||||
|
|
||||
|
||||
S404.py:3:6: S404 `subprocess` module is possibly insecure
|
||||
|
|
||||
1 | import subprocess # S404
|
||||
2 | from subprocess import Popen # S404
|
||||
3 | from subprocess import Popen as pop # S404
|
||||
| ^^^^^^^^^^ S404
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S405.py:1:8: S405 `xml.etree` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | import xml.etree.cElementTree # S405
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ S405
|
||||
2 | from xml.etree import cElementTree # S405
|
||||
3 | import xml.etree.ElementTree # S405
|
||||
|
|
||||
|
||||
S405.py:2:6: S405 `xml.etree` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | import xml.etree.cElementTree # S405
|
||||
2 | from xml.etree import cElementTree # S405
|
||||
| ^^^^^^^^^ S405
|
||||
3 | import xml.etree.ElementTree # S405
|
||||
4 | from xml.etree import ElementTree # S405
|
||||
|
|
||||
|
||||
S405.py:3:8: S405 `xml.etree` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | import xml.etree.cElementTree # S405
|
||||
2 | from xml.etree import cElementTree # S405
|
||||
3 | import xml.etree.ElementTree # S405
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S405
|
||||
4 | from xml.etree import ElementTree # S405
|
||||
|
|
||||
|
||||
S405.py:4:6: S405 `xml.etree` methods are vulnerable to XML attacks
|
||||
|
|
||||
2 | from xml.etree import cElementTree # S405
|
||||
3 | import xml.etree.ElementTree # S405
|
||||
4 | from xml.etree import ElementTree # S405
|
||||
| ^^^^^^^^^ S405
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S406.py:1:6: S406 `xml.sax` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml import sax # S406
|
||||
| ^^^ S406
|
||||
2 | import xml.sax as xmls # S406
|
||||
3 | import xml.sax # S406
|
||||
|
|
||||
|
||||
S406.py:2:8: S406 `xml.sax` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml import sax # S406
|
||||
2 | import xml.sax as xmls # S406
|
||||
| ^^^^^^^^^^^^^^^ S406
|
||||
3 | import xml.sax # S406
|
||||
|
|
||||
|
||||
S406.py:3:8: S406 `xml.sax` methods are vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml import sax # S406
|
||||
2 | import xml.sax as xmls # S406
|
||||
3 | import xml.sax # S406
|
||||
| ^^^^^^^ S406
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S407.py:1:6: S407 `xml.dom.expatbuilder` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom import expatbuilder # S407
|
||||
| ^^^^^^^ S407
|
||||
2 | import xml.dom.expatbuilder # S407
|
||||
|
|
||||
|
||||
S407.py:2:8: S407 `xml.dom.expatbuilder` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom import expatbuilder # S407
|
||||
2 | import xml.dom.expatbuilder # S407
|
||||
| ^^^^^^^^^^^^^^^^^^^^ S407
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S408.py:1:6: S408 `xml.dom.minidom` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom.minidom import parseString # S408
|
||||
| ^^^^^^^^^^^^^^^ S408
|
||||
2 | import xml.dom.minidom # S408
|
||||
|
|
||||
|
||||
S408.py:2:8: S408 `xml.dom.minidom` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom.minidom import parseString # S408
|
||||
2 | import xml.dom.minidom # S408
|
||||
| ^^^^^^^^^^^^^^^ S408
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S409.py:1:6: S409 `xml.dom.pulldom` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom.pulldom import parseString # S409
|
||||
| ^^^^^^^^^^^^^^^ S409
|
||||
2 | import xml.dom.pulldom # S409
|
||||
|
|
||||
|
||||
S409.py:2:8: S409 `xml.dom.pulldom` is vulnerable to XML attacks
|
||||
|
|
||||
1 | from xml.dom.pulldom import parseString # S409
|
||||
2 | import xml.dom.pulldom # S409
|
||||
| ^^^^^^^^^^^^^^^ S409
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S410.py:1:8: S410 `lxml` is vulnerable to XML attacks
|
||||
|
|
||||
1 | import lxml # S410
|
||||
| ^^^^ S410
|
||||
2 | from lxml import etree # S410
|
||||
|
|
||||
|
||||
S410.py:2:6: S410 `lxml` is vulnerable to XML attacks
|
||||
|
|
||||
1 | import lxml # S410
|
||||
2 | from lxml import etree # S410
|
||||
| ^^^^ S410
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S411.py:1:8: S411 XMLRPC is vulnerable to remote XML attacks
|
||||
|
|
||||
1 | import xmlrpc # S411
|
||||
| ^^^^^^ S411
|
||||
2 | from xmlrpc import server # S411
|
||||
|
|
||||
|
||||
S411.py:2:6: S411 XMLRPC is vulnerable to remote XML attacks
|
||||
|
|
||||
1 | import xmlrpc # S411
|
||||
2 | from xmlrpc import server # S411
|
||||
| ^^^^^^ S411
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S412.py:1:6: S412 `httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided
|
||||
|
|
||||
1 | from twisted.web.twcgi import CGIScript # S412
|
||||
| ^^^^^^^^^^^^^^^^^ S412
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S413.py:1:8: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
||||
|
|
||||
1 | import Crypto.Hash # S413
|
||||
| ^^^^^^^^^^^ S413
|
||||
2 | from Crypto.Hash import MD2 # S413
|
||||
3 | import Crypto.PublicKey # S413
|
||||
|
|
||||
|
||||
S413.py:2:6: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
||||
|
|
||||
1 | import Crypto.Hash # S413
|
||||
2 | from Crypto.Hash import MD2 # S413
|
||||
| ^^^^^^^^^^^ S413
|
||||
3 | import Crypto.PublicKey # S413
|
||||
4 | from Crypto.PublicKey import RSA # S413
|
||||
|
|
||||
|
||||
S413.py:3:8: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
||||
|
|
||||
1 | import Crypto.Hash # S413
|
||||
2 | from Crypto.Hash import MD2 # S413
|
||||
3 | import Crypto.PublicKey # S413
|
||||
| ^^^^^^^^^^^^^^^^ S413
|
||||
4 | from Crypto.PublicKey import RSA # S413
|
||||
|
|
||||
|
||||
S413.py:4:6: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
||||
|
|
||||
2 | from Crypto.Hash import MD2 # S413
|
||||
3 | import Crypto.PublicKey # S413
|
||||
4 | from Crypto.PublicKey import RSA # S413
|
||||
| ^^^^^^^^^^^^^^^^ S413
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S415.py:1:8: S415 An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.
|
||||
|
|
||||
1 | import pyghmi # S415
|
||||
| ^^^^^^ S415
|
||||
2 | from pyghmi import foo # S415
|
||||
|
|
||||
|
||||
S415.py:2:6: S415 An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.
|
||||
|
|
||||
1 | import pyghmi # S415
|
||||
2 | from pyghmi import foo # S415
|
||||
| ^^^^^^ S415
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S502.py:6:13: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv3`
|
||||
|
|
||||
4 | from OpenSSL.SSL import Context
|
||||
5 |
|
||||
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
|
||||
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
|
|
||||
|
||||
S502.py:7:17: S502 Call made with insecure SSL protocol: `PROTOCOL_TLSv1`
|
||||
|
|
||||
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
|
||||
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
|
|
||||
|
||||
S502.py:8:17: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv2`
|
||||
|
|
||||
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
|
||||
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
|
||||
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
|
|
||||
|
||||
S502.py:9:13: S502 Call made with insecure SSL protocol: `SSLv2_METHOD`
|
||||
|
|
||||
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
|
||||
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
11 | Context(method=SSL.SSLv3_METHOD) # S502
|
||||
|
|
||||
|
||||
S502.py:10:13: S502 Call made with insecure SSL protocol: `SSLv23_METHOD`
|
||||
|
|
||||
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
||||
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
11 | Context(method=SSL.SSLv3_METHOD) # S502
|
||||
12 | Context(method=SSL.TLSv1_METHOD) # S502
|
||||
|
|
||||
|
||||
S502.py:11:9: S502 Call made with insecure SSL protocol: `SSLv3_METHOD`
|
||||
|
|
||||
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
||||
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
11 | Context(method=SSL.SSLv3_METHOD) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
12 | Context(method=SSL.TLSv1_METHOD) # S502
|
||||
|
|
||||
|
||||
S502.py:12:9: S502 Call made with insecure SSL protocol: `TLSv1_METHOD`
|
||||
|
|
||||
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
||||
11 | Context(method=SSL.SSLv3_METHOD) # S502
|
||||
12 | Context(method=SSL.TLSv1_METHOD) # S502
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
|
||||
13 |
|
||||
14 | wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S503.py:6:18: S503 Argument default set to insecure SSL protocol: `PROTOCOL_SSLv2`
|
||||
|
|
||||
6 | def func(version=ssl.PROTOCOL_SSLv2): # S503
|
||||
| ^^^^^^^^^^^^^^^^^^ S503
|
||||
7 | pass
|
||||
|
|
||||
|
||||
S503.py:10:19: S503 Argument default set to insecure SSL protocol: `SSLv2_METHOD`
|
||||
|
|
||||
10 | def func(protocol=SSL.SSLv2_METHOD): # S503
|
||||
| ^^^^^^^^^^^^^^^^ S503
|
||||
11 | pass
|
||||
|
|
||||
|
||||
S503.py:14:18: S503 Argument default set to insecure SSL protocol: `SSLv23_METHOD`
|
||||
|
|
||||
14 | def func(version=SSL.SSLv23_METHOD): # S503
|
||||
| ^^^^^^^^^^^^^^^^^ S503
|
||||
15 | pass
|
||||
|
|
||||
|
||||
S503.py:18:19: S503 Argument default set to insecure SSL protocol: `PROTOCOL_TLSv1`
|
||||
|
|
||||
18 | def func(protocol=PROTOCOL_TLSv1): # S503
|
||||
| ^^^^^^^^^^^^^^ S503
|
||||
19 | pass
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S504.py:4:1: S504 `ssl.wrap_socket` called without an `ssl_version``
|
||||
|
|
||||
2 | from ssl import wrap_socket
|
||||
3 |
|
||||
4 | ssl.wrap_socket() # S504
|
||||
| ^^^^^^^^^^^^^^^^^ S504
|
||||
5 | wrap_socket() # S504
|
||||
6 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|
||||
|
|
||||
|
||||
S504.py:5:1: S504 `ssl.wrap_socket` called without an `ssl_version``
|
||||
|
|
||||
4 | ssl.wrap_socket() # S504
|
||||
5 | wrap_socket() # S504
|
||||
| ^^^^^^^^^^^^^ S504
|
||||
6 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|
||||
|
|
||||
|
||||
|
||||
@@ -51,13 +51,29 @@ pub(super) fn is_allowed_func_def(name: &str) -> bool {
|
||||
/// Returns `true` if an argument is allowed to use a boolean trap. To return
|
||||
/// `true`, the function name must be explicitly allowed, and the argument must
|
||||
/// be either the first or second argument in the call.
|
||||
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
|
||||
return is_allowed_func_call(attr);
|
||||
pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
|
||||
let func_name = match call.func.as_ref() {
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr.as_str(),
|
||||
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
// If the function name is explicitly allowed, then the boolean trap is
|
||||
// allowed.
|
||||
if is_allowed_func_call(func_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
return is_allowed_func_call(id);
|
||||
// If the function appears to be a setter (e.g., `set_visible` or `setVisible`), then the
|
||||
// boolean trap is allowed. We want to avoid raising a violation for cases in which the argument
|
||||
// is positional-only and third-party, and this tends to be the case for setters.
|
||||
if call.arguments.args.len() == 1 {
|
||||
if func_name
|
||||
.strip_prefix("set")
|
||||
.is_some_and(|suffix| suffix.starts_with(|c: char| c == '_' || c.is_ascii_uppercase()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -45,11 +45,16 @@ impl Violation for BooleanPositionalValueInCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
|
||||
if allow_boolean_trap(func) {
|
||||
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if allow_boolean_trap(call) {
|
||||
return;
|
||||
}
|
||||
for arg in args.iter().filter(|arg| arg.is_boolean_literal_expr()) {
|
||||
for arg in call
|
||||
.arguments
|
||||
.args
|
||||
.iter()
|
||||
.filter(|arg| arg.is_boolean_literal_expr())
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));
|
||||
|
||||
@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
|
||||
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
88 | # FBT001: Boolean positional arg in function definition
|
||||
89 | def foo(self, value: bool) -> None:
|
||||
90 | # FBT001: Boolean positional arg in function definition
|
||||
91 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^ FBT001
|
||||
90 | pass
|
||||
92 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -28,14 +28,10 @@ FBT.py:57:17: FBT003 Boolean positional value in function call
|
||||
59 | mylist.index(True)
|
||||
|
|
||||
|
||||
FBT.py:69:38: FBT003 Boolean positional value in function call
|
||||
|
|
||||
67 | os.set_blocking(0, False)
|
||||
68 | g_action.set_enabled(True)
|
||||
69 | settings.set_enable_developer_extras(True)
|
||||
| ^^^^ FBT003
|
||||
70 | foo.is_(True)
|
||||
71 | bar.is_not(False)
|
||||
|
|
||||
FBT.py:121:10: FBT003 Boolean positional value in function call
|
||||
|
|
||||
121 | settings(True)
|
||||
| ^^^^ FBT003
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -81,26 +81,26 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
|
||||
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
88 | # FBT001: Boolean positional arg in function definition
|
||||
89 | def foo(self, value: bool) -> None:
|
||||
90 | # FBT001: Boolean positional arg in function definition
|
||||
91 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^ FBT001
|
||||
90 | pass
|
||||
92 | pass
|
||||
|
|
||||
|
||||
FBT.py:99:10: FBT001 Boolean-typed positional argument in function definition
|
||||
FBT.py:101:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
99 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
101 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
| ^ FBT001
|
||||
100 | pass
|
||||
102 | pass
|
||||
|
|
||||
|
||||
FBT.py:103:10: FBT001 Boolean-typed positional argument in function definition
|
||||
FBT.py:105:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
103 | def func(x: bool | str):
|
||||
105 | def func(x: bool | str):
|
||||
| ^ FBT001
|
||||
104 | pass
|
||||
106 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,31 @@ impl Violation for LoopVariableOverridesIterator {
|
||||
}
|
||||
}
|
||||
|
||||
/// B020
|
||||
pub(crate) fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
|
||||
let target_names = {
|
||||
let mut target_finder = NameFinder::default();
|
||||
target_finder.visit_expr(target);
|
||||
target_finder.names
|
||||
};
|
||||
let iter_names = {
|
||||
let mut iter_finder = NameFinder::default();
|
||||
iter_finder.visit_expr(iter);
|
||||
iter_finder.names
|
||||
};
|
||||
|
||||
for (name, expr) in target_names {
|
||||
if iter_names.contains_key(name) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
LoopVariableOverridesIterator {
|
||||
name: name.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NameFinder<'a> {
|
||||
names: FxHashMap<&'a str, &'a Expr>,
|
||||
@@ -97,28 +122,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// B020
|
||||
pub(crate) fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
|
||||
let target_names = {
|
||||
let mut target_finder = NameFinder::default();
|
||||
target_finder.visit_expr(target);
|
||||
target_finder.names
|
||||
};
|
||||
let iter_names = {
|
||||
let mut iter_finder = NameFinder::default();
|
||||
iter_finder.visit_expr(iter);
|
||||
iter_finder.names
|
||||
};
|
||||
|
||||
for (name, expr) in target_names {
|
||||
if iter_names.contains_key(name) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
LoopVariableOverridesIterator {
|
||||
name: name.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::helpers;
|
||||
use ruff_python_ast::helpers::NameFinder;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_ast::{helpers, visitor};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -78,42 +77,16 @@ impl Violation for UnusedLoopControlVariable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Identify all `Expr::Name` nodes in an AST.
|
||||
struct NameFinder<'a> {
|
||||
/// A map from identifier to defining expression.
|
||||
names: FxHashMap<&'a str, &'a Expr>,
|
||||
}
|
||||
|
||||
impl NameFinder<'_> {
|
||||
fn new() -> Self {
|
||||
NameFinder {
|
||||
names: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = expr {
|
||||
self.names.insert(id, expr);
|
||||
}
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
/// B007
|
||||
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
|
||||
let control_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
let mut finder = NameFinder::default();
|
||||
finder.visit_expr(stmt_for.target.as_ref());
|
||||
finder.names
|
||||
};
|
||||
|
||||
let used_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
let mut finder = NameFinder::default();
|
||||
for stmt in &stmt_for.body {
|
||||
finder.visit_stmt(stmt);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Arguments, Decorator};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::{BindingKind, Scope, ScopeId};
|
||||
use ruff_source_file::SourceRow;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
@@ -20,6 +19,23 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// non-obvious errors, as readers may mistake the attribute for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Since methods and class attributes typically cannot be referenced directly
|
||||
/// from outside the class scope, this rule only applies to those methods
|
||||
/// and attributes that both shadow a builtin _and_ are referenced from within
|
||||
/// the class scope, as in the following example, where the `list[int]` return
|
||||
/// type annotation resolves to the `list` method, rather than the builtin:
|
||||
///
|
||||
/// ```python
|
||||
/// class Class:
|
||||
/// @staticmethod
|
||||
/// def list() -> None:
|
||||
/// pass
|
||||
///
|
||||
/// @staticmethod
|
||||
/// def repeat(value: int, times: int) -> list[int]:
|
||||
/// return [value] * times
|
||||
/// ```
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// converted to the appropriate dunder method. Methods decorated with
|
||||
@@ -28,135 +44,112 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def int():
|
||||
/// return 0
|
||||
/// ```
|
||||
/// class Class:
|
||||
/// @staticmethod
|
||||
/// def list() -> None:
|
||||
/// pass
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def to_int():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// Or:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// # Callable as `int(shadow)`
|
||||
/// def __int__():
|
||||
/// return 0
|
||||
/// @staticmethod
|
||||
/// def repeat(value: int, times: int) -> list[int]:
|
||||
/// return [value] * times
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinAttributeShadowing {
|
||||
kind: Kind,
|
||||
name: String,
|
||||
row: SourceRow,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinAttributeShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinAttributeShadowing { name } = self;
|
||||
format!("Class attribute `{name}` is shadowing a Python builtin")
|
||||
let BuiltinAttributeShadowing { kind, name, row } = self;
|
||||
match kind {
|
||||
Kind::Attribute => {
|
||||
format!("Python builtin is shadowed by class attribute `{name}` from {row}")
|
||||
}
|
||||
Kind::Method => {
|
||||
format!("Python builtin is shadowed by method `{name}` from {row}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A003
|
||||
pub(crate) fn builtin_attribute_shadowing(
|
||||
checker: &mut Checker,
|
||||
checker: &Checker,
|
||||
scope_id: ScopeId,
|
||||
scope: &Scope,
|
||||
class_def: &ast::StmtClassDef,
|
||||
name: &str,
|
||||
range: TextRange,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
|
||||
// subscripting and not through attribute access.
|
||||
if class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| checker.semantic().match_typing_expr(base, "TypedDict"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (name, binding_id) in scope.all_bindings() {
|
||||
let binding = checker.semantic().binding(binding_id);
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
range,
|
||||
));
|
||||
// We only care about methods and attributes.
|
||||
let kind = match binding.kind {
|
||||
BindingKind::Assignment | BindingKind::Annotation => Kind::Attribute,
|
||||
BindingKind::FunctionDefinition(_) => Kind::Method,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
// Ignore explicit overrides.
|
||||
if class_def.decorator_list.iter().any(|decorator| {
|
||||
checker
|
||||
.semantic()
|
||||
.match_typing_expr(&decorator.expression, "override")
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Class scopes are special, in that you can only reference a binding defined in a
|
||||
// class scope from within the class scope itself. As such, we can safely ignore
|
||||
// methods that weren't referenced from within the class scope. In other words, we're
|
||||
// only trying to identify shadowing as in:
|
||||
// ```python
|
||||
// class Class:
|
||||
// @staticmethod
|
||||
// def list() -> None:
|
||||
// pass
|
||||
//
|
||||
// @staticmethod
|
||||
// def repeat(value: int, times: int) -> list[int]:
|
||||
// return [value] * times
|
||||
// ```
|
||||
for reference in binding
|
||||
.references
|
||||
.iter()
|
||||
.map(|reference_id| checker.semantic().reference(*reference_id))
|
||||
.filter(|reference| {
|
||||
checker
|
||||
.semantic()
|
||||
.first_non_type_parent_scope_id(reference.scope_id())
|
||||
== Some(scope_id)
|
||||
})
|
||||
{
|
||||
diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
kind,
|
||||
name: name.to_string(),
|
||||
row: checker.compute_source_row(binding.start()),
|
||||
},
|
||||
reference.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A003
|
||||
pub(crate) fn builtin_method_shadowing(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
name: &str,
|
||||
decorator_list: &[Decorator],
|
||||
range: TextRange,
|
||||
) {
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
|
||||
// those should be flagged on the superclass, but that's more difficult.
|
||||
if is_standard_library_override(name, class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore explicit overrides.
|
||||
if decorator_list.iter().any(|decorator| {
|
||||
checker
|
||||
.semantic()
|
||||
.match_typing_expr(&decorator.expression, "override")
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
range,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if an attribute appears to be an override of a standard-library method.
|
||||
fn is_standard_library_override(
|
||||
name: &str,
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
|
||||
return false;
|
||||
};
|
||||
match name {
|
||||
// Ex) `Event.set`
|
||||
"set" => bases.iter().any(|base| {
|
||||
semantic
|
||||
.resolve_call_path(base)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
|
||||
}),
|
||||
// Ex) `Filter.filter`
|
||||
"filter" => bases.iter().any(|base| {
|
||||
semantic
|
||||
.resolve_call_path(base)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
Attribute,
|
||||
Method,
|
||||
}
|
||||
|
||||
@@ -1,68 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
|
||||
---
|
||||
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
||||
|
|
||||
1 | class MyClass:
|
||||
2 | ImportError = 4
|
||||
| ^^^^^^^^^^^ A003
|
||||
3 | id: int
|
||||
4 | dir = "/"
|
||||
|
|
||||
|
||||
A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin
|
||||
|
|
||||
1 | class MyClass:
|
||||
2 | ImportError = 4
|
||||
3 | id: int
|
||||
| ^^ A003
|
||||
4 | dir = "/"
|
||||
|
|
||||
|
||||
A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin
|
||||
|
|
||||
2 | ImportError = 4
|
||||
3 | id: int
|
||||
4 | dir = "/"
|
||||
| ^^^ A003
|
||||
5 |
|
||||
6 | def __init__(self):
|
||||
|
|
||||
|
||||
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
|
||||
|
|
||||
9 | self.dir = "."
|
||||
10 |
|
||||
11 | def str(self):
|
||||
| ^^^ A003
|
||||
12 | pass
|
||||
15 | pass
|
||||
16 |
|
||||
17 | def method_usage(self) -> str:
|
||||
| ^^^ A003
|
||||
18 | pass
|
||||
|
|
||||
|
||||
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
A003.py:20:34: A003 Python builtin is shadowed by class attribute `id` from line 3
|
||||
|
|
||||
27 | ...
|
||||
28 |
|
||||
29 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
30 | ...
|
||||
|
|
||||
|
||||
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
|
|
||||
38 | ...
|
||||
39 |
|
||||
40 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
41 | ...
|
||||
|
|
||||
|
||||
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
||||
|
|
||||
50 | pass
|
||||
51 |
|
||||
52 | def int(self):
|
||||
| ^^^ A003
|
||||
53 | pass
|
||||
18 | pass
|
||||
19 |
|
||||
20 | def attribute_usage(self) -> id:
|
||||
| ^^ A003
|
||||
21 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user