Compare commits

..

2 Commits

Author SHA1 Message Date
Charlie Marsh
603d3e1984 Add quoted annotation rules 2023-07-14 16:16:21 -04:00
Charlie Marsh
402b3c7f04 Expand scope of quoted-annotation rule 2023-07-14 16:15:38 -04:00
162 changed files with 2865 additions and 4428 deletions

2
.gitignore vendored
View File

@@ -10,7 +10,7 @@ schemastore
# `maturin develop` and ecosystem_all_check.sh
.venv*
# Formatter debugging (crates/ruff_python_formatter/README.md)
scratch.*
scratch.py
# Created by `perf` (CONTRIBUTING.md)
perf.data
perf.data.old

View File

@@ -110,35 +110,27 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
If you're working on a rule, this is the crate for you.
- `crates/ruff_benchmark`: binary crate for running micro-benchmarks.
- `crates/ruff_cache`: library crate for caching lint results.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`), see the [`cargo dev`](#cargo-dev) section below.
- `crates/ruff_diagnostics`: library crate for the rule-independent abstractions in the lint
diagnostics APIs.
- `crates/ruff_formatter`: library crate for language agnostic code formatting logic based on an
intermediate representation. The backend for `ruff_python_formatter`.
`cargo dev generate-all`).
- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs.
- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate
representation.
- `crates/ruff_index`: library crate inspired by `rustc_index`.
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities. Note
that the AST schema itself is defined in the
[rustpython-ast](https://github.com/astral-sh/RustPython-Parser) crate.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic.
- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic,
including Ruff's semantic model. Used to resolve queries like "What import does this variable
refer to?"
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data, e.g.
the names of all built-in exceptions and which standard library types are immutable.
including Ruff's semantic model.
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data.
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
logic (indentation and newlines).
logic.
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff.
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the
[Ruff Playground](https://play.ruff.rs/).
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module.
### Example: Adding a new lint rule
@@ -419,13 +411,6 @@ Summary
159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython'
```
To benchmark a subset of rules, e.g. `LineTooLong` and `DocLineTooLong`:
```shell
cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e --select W505,E501"
```
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.

281
Cargo.lock generated
View File

@@ -14,6 +14,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@@ -111,9 +120,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.72"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "argfile"
@@ -126,9 +135,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.12"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151"
dependencies = [
"anstyle",
"bstr",
@@ -270,9 +279,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.14"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98330784c494e49850cb23b8e2afcca13587d2500b2e3f1f78ae20248059c9be"
checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -281,9 +290,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.14"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182eb5f2562a67dda37e2c57af64d720a9e010c5e860ed87c056586aeafa52e"
checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
dependencies = [
"anstream",
"anstyle",
@@ -334,14 +343,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.3.12"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -526,10 +535,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "darling"
version = "0.20.3"
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "darling"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
dependencies = [
"darling_core",
"darling_macro",
@@ -537,27 +556,27 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.3"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
name = "darling_macro"
version = "0.20.3"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -627,9 +646,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
[[package]]
name = "dyn-clone"
version = "1.0.12"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
[[package]]
name = "either"
@@ -658,9 +677,9 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
@@ -785,11 +804,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.11"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [
"aho-corasick",
"aho-corasick 0.7.20",
"bstr",
"fnv",
"log",
@@ -944,12 +963,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "indoc"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
[[package]]
name = "inotify"
version = "0.9.6"
@@ -972,9 +985,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.31.0"
version = "1.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a"
checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3"
dependencies = [
"console",
"globset",
@@ -1020,12 +1033,12 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.9"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
dependencies = [
"hermit-abi",
"rustix 0.38.4",
"rustix 0.38.3",
"windows-sys 0.48.0",
]
@@ -1040,9 +1053,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.9"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "js-sys"
@@ -1368,10 +1381,19 @@ dependencies = [
]
[[package]]
name = "paste"
version = "1.0.14"
name = "output_vt100"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
dependencies = [
"winapi",
]
[[package]]
name = "paste"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
[[package]]
name = "path-absolutize"
@@ -1498,7 +1520,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -1557,9 +1579,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.4.1"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6"
checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
[[package]]
name = "predicates"
@@ -1591,11 +1613,13 @@ dependencies = [
[[package]]
name = "pretty_assertions"
version = "1.4.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
dependencies = [
"ctor",
"diff",
"output_vt100",
"yansi",
]
@@ -1625,9 +1649,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.66"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
@@ -1647,12 +1671,12 @@ dependencies = [
[[package]]
name = "quick-junit"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd"
dependencies = [
"chrono",
"indexmap 2.0.0",
"indexmap 1.9.3",
"nextest-workspace-hack",
"quick-xml",
"thiserror",
@@ -1661,18 +1685,18 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.29.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
@@ -1745,11 +1769,11 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
dependencies = [
"aho-corasick",
"aho-corasick 1.0.2",
"memchr",
"regex-automata",
"regex-syntax",
@@ -1757,20 +1781,20 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.3"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
dependencies = [
"aho-corasick",
"aho-corasick 1.0.2",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.4"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
[[package]]
name = "result-like"
@@ -1963,7 +1987,6 @@ dependencies = [
"clap",
"ignore",
"indicatif",
"indoc",
"itertools",
"libcst",
"log",
@@ -1981,13 +2004,11 @@ dependencies = [
"rustpython-format",
"rustpython-parser",
"schemars",
"serde",
"serde_json",
"similar",
"strum",
"strum_macros",
"tempfile",
"toml",
]
[[package]]
@@ -2031,7 +2052,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_textwrap",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2134,7 +2155,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"schemars",
"serde",
@@ -2199,9 +2220,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.4"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
dependencies = [
"bitflags 2.3.3",
"errno",
@@ -2212,13 +2233,13 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.5"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.1",
"rustls-webpki",
"sct",
]
@@ -2232,20 +2253,10 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.101.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"is-macro",
"num-bigint",
@@ -2256,7 +2267,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"bitflags 2.3.3",
"itertools",
@@ -2268,7 +2279,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2280,7 +2291,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"anyhow",
"is-macro",
@@ -2303,7 +2314,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
dependencies = [
"is-macro",
"memchr",
@@ -2312,15 +2323,15 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.14"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
[[package]]
name = "ryu"
version = "1.0.15"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "same-file"
@@ -2363,9 +2374,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
@@ -2379,15 +2390,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.171"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
dependencies = [
"serde_derive",
]
@@ -2405,13 +2416,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.171"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2427,9 +2438,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.103"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
@@ -2447,9 +2458,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.1.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
dependencies = [
"base64",
"chrono",
@@ -2458,19 +2469,19 @@ dependencies = [
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.23",
"time 0.3.22",
]
[[package]]
name = "serde_with_macros"
version = "3.1.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2496,9 +2507,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "spin"
@@ -2553,9 +2564,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.26"
version = "2.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
dependencies = [
"proc-macro2",
"quote",
@@ -2665,7 +2676,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2711,9 +2722,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.23"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
dependencies = [
"itoa",
"serde",
@@ -2729,9 +2740,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.10"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
@@ -2772,9 +2783,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.6"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
dependencies = [
"serde",
"serde_spanned",
@@ -2793,9 +2804,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.14"
version = "0.19.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
dependencies = [
"indexmap 2.0.0",
"serde",
@@ -2825,7 +2836,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2915,9 +2926,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.11"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "unicode-normalization"
@@ -2959,7 +2970,7 @@ dependencies = [
"log",
"once_cell",
"rustls",
"rustls-webpki 0.100.1",
"rustls-webpki",
"url",
"webpki-roots",
]
@@ -2984,9 +2995,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "version_check"
@@ -3046,7 +3057,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
"wasm-bindgen-shared",
]
@@ -3080,7 +3091,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3131,7 +3142,7 @@ version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki 0.100.1",
"rustls-webpki",
]
[[package]]
@@ -3328,9 +3339,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.0"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
dependencies = [
"memchr",
]

View File

@@ -54,12 +54,12 @@ libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f
# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
# Note: As of tag v0.0.8 we are cherry-picking commits instead of rebasing so the tag is not necessary
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" , default-features = false, features = ["full-lexer", "num-bigint"] }
# Current tag: v0.0.7
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] }
[profile.release]
lto = "fat"

View File

@@ -177,9 +177,6 @@ def str_okay(value=str("foo")):
def bool_okay(value=bool("bar")):
pass
# Allow immutable bytes() value
def bytes_okay(value=bytes(1)):
pass
# Allow immutable int() value
def int_okay(value=int("12")):

View File

@@ -0,0 +1,21 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import module
from module import Class
def f(var: Class) -> Class:
x: Class
def f(var: module.Class) -> module.Class:
x: module.Class
def f():
print(Class)
def f():
print(module.Class)

View File

@@ -0,0 +1,6 @@
from typing import TYPE_CHECKING
Class = ...
if TYPE_CHECKING:
from module import Class

View File

@@ -1,27 +0,0 @@
import pandas as pd
data = pd.Series(range(1000))
# PD101
data.nunique() <= 1
data.nunique(dropna=True) <= 1
data.nunique(dropna=False) <= 1
data.nunique() == 1
data.nunique(dropna=True) == 1
data.nunique(dropna=False) == 1
data.nunique() != 1
data.nunique(dropna=True) != 1
data.nunique(dropna=False) != 1
data.nunique() > 1
data.dropna().nunique() == 1
data[data.notnull()].nunique() == 1
# No violation of this rule
data.nunique() == 0 # empty
data.nunique() >= 1 # not-empty
data.nunique() < 1 # empty
data.nunique() == 2 # not constant
data.unique() == 1 # not `nunique`
{"hello": "world"}.nunique() == 1 # no pd.Series

View File

@@ -1,20 +0,0 @@
import pandas as pd
# Errors.
df = pd.read_table("data.csv", sep=",")
df = pd.read_table("data.csv", sep=",", header=0)
filename = "data.csv"
df = pd.read_table(filename, sep=",")
df = pd.read_table(filename, sep=",", header=0)
# Non-errors.
df = pd.read_csv("data.csv")
df = pd.read_table("data.tsv")
df = pd.read_table("data.tsv", sep="\t")
df = pd.read_table("data.tsv", sep=",,")
df = pd.read_table("data.tsv", sep=", ")
df = pd.read_table("data.tsv", sep=" ,")
df = pd.read_table("data.tsv", sep=" , ")
not_pd.read_table("data.csv", sep=",")
data = read_table("data.csv", sep=",")
data = read_table

View File

@@ -1,101 +1,71 @@
some_dict = {"a": 12, "b": 32, "c": 44}
def f():
for _, value in some_dict.items(): # PERF102
print(value)
for _, value in some_dict.items(): # PERF102
print(value)
def f():
for key, _ in some_dict.items(): # PERF102
print(key)
for key, _ in some_dict.items(): # PERF102
print(key)
def f():
for weird_arg_name, _ in some_dict.items(): # PERF102
print(weird_arg_name)
for weird_arg_name, _ in some_dict.items(): # PERF102
print(weird_arg_name)
def f():
for name, (_, _) in some_dict.items(): # PERF102
print(name)
for name, (_, _) in some_dict.items(): # PERF102
pass
def f():
for name, (value1, _) in some_dict.items(): # OK
print(name, value1)
for name, (value1, _) in some_dict.items(): # OK
pass
def f():
for (key1, _), (_, _) in some_dict.items(): # PERF102
print(key1)
for (key1, _), (_, _) in some_dict.items(): # PERF102
pass
def f():
for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
print(value)
for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
pass
def f():
for (_, key2), (value1, _) in some_dict.items(): # OK
print(key2, value1)
for (_, key2), (value1, _) in some_dict.items(): # OK
pass
def f():
for ((_, key2), (value1, _)) in some_dict.items(): # OK
print(key2, value1)
for ((_, key2), (value1, _)) in some_dict.items(): # OK
pass
def f():
for ((_, key2), (_, _)) in some_dict.items(): # PERF102
print(key2)
for ((_, key2), (_, _)) in some_dict.items(): # PERF102
pass
def f():
for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK
print(variants, r_language)
for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK
pass
def f():
for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK
print(variants, r_language)
for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK
pass
def f():
for key, value in some_dict.items(): # OK
print(key, value)
for key, value in some_dict.items(): # OK
print(key, value)
def f():
for _, value in some_dict.items(12): # OK
print(value)
for _, value in some_dict.items(12): # OK
print(value)
def f():
for key in some_dict.keys(): # OK
print(key)
for key in some_dict.keys(): # OK
print(key)
def f():
for value in some_dict.values(): # OK
print(value)
for value in some_dict.values(): # OK
print(value)
def f():
for name, (_, _) in (some_function()).items(): # PERF102
print(name)
for name, (_, _) in (some_function()).items(): # PERF102
pass
def f():
for name, (_, _) in (some_function().some_attribute).items(): # PERF102
print(name)
def f():
for name, unused_value in some_dict.items(): # PERF102
print(name)
def f():
for unused_name, value in some_dict.items(): # PERF102
print(value)
for name, (_, _) in (some_function().some_attribute).items(): # PERF102
pass

View File

@@ -80,8 +80,3 @@ def multiple_assignment():
global CONSTANT # [global-statement]
CONSTANT = 1
CONSTANT = 2
def no_assignment():
"""Shouldn't warn"""
global CONSTANT

View File

@@ -4,9 +4,23 @@ import typing
# with complex annotations
MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
# with default values as list
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str), ("c", list[bool])],
defaults=["foo", [True]],
)
# with namespace
MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
# too many default values (OK)
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str)],
defaults=[1, "bar", "baz"],
)
# invalid identifiers (OK)
MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])
@@ -15,10 +29,3 @@ MyType = typing.NamedTuple("MyType")
# empty fields
MyType = typing.NamedTuple("MyType", [])
# keywords
MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)

File diff suppressed because it is too large Load Diff

View File

@@ -115,6 +115,7 @@ pub(crate) fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
} else {
}
}
}

View File

@@ -604,7 +604,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(PandasVet, "012") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotReadTable),
(PandasVet, "013") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotStack),
(PandasVet, "015") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfPdMerge),
(PandasVet, "101") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck),
(PandasVet, "901") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasDfVariableName),
// flake8-errmsg
@@ -708,6 +707,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "003") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
(Flake8TypeChecking, "004") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "200") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::UnquotedAnnotation),
// tryceratops
(Tryceratops, "002") => (RuleGroup::Unspecified, rules::tryceratops::rules::RaiseVanillaClass),

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -29,7 +30,12 @@ impl Violation for Jinja2AutoescapeFalse {
}
/// S701
pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn jinja2_autoescape_false(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker
.semantic()
.resolve_call_path(func)
@@ -37,13 +43,10 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywor
matches!(call_path.as_slice(), ["jinja2", "Environment"])
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "autoescape")
}) {
match &keyword.value {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(autoescape_arg) = call_args.keyword_argument("autoescape") {
match autoescape_arg {
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),
..
@@ -53,14 +56,14 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywor
if id != "select_autoescape" {
checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
keyword.range(),
autoescape_arg.range(),
));
}
}
}
_ => checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
keyword.range(),
autoescape_arg.range(),
)),
}
} else {

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -19,6 +20,7 @@ impl Violation for LoggingConfigInsecureListen {
pub(crate) fn logging_config_insecure_listen(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker
@@ -28,17 +30,12 @@ pub(crate) fn logging_config_insecure_listen(
matches!(call_path.as_slice(), ["logging", "config", "listen"])
})
{
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
return;
}
let call_args = SimpleCallArgs::new(args, keywords);
checker
.diagnostics
.push(Diagnostic::new(LoggingConfigInsecureListen, func.range()));
if call_args.keyword_argument("verify").is_none() {
checker
.diagnostics
.push(Diagnostic::new(LoggingConfigInsecureListen, func.range()));
}
}
}

View File

@@ -2,34 +2,10 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_false;
use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for HTTPS requests that disable SSL certificate checks.
///
/// ## Why is this bad?
/// If SSL certificates are not verified, an attacker could perform a "man in
/// the middle" attack by intercepting and modifying traffic between the client
/// and server.
///
/// ## Example
/// ```python
/// import requests
///
/// requests.get("https://www.example.com", verify=False)
/// ```
///
/// Use instead:
/// ```python
/// import requests
///
/// requests.get("https://www.example.com") # By default, `verify=True`.
/// ```
///
/// ## References
/// - [Common Weakness Enumeration: CWE-295](https://cwe.mitre.org/data/definitions/295.html)
#[violation]
pub struct RequestWithNoCertValidation {
string: String,
@@ -49,6 +25,7 @@ impl Violation for RequestWithNoCertValidation {
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if let Some(target) = checker
@@ -63,18 +40,14 @@ pub(crate) fn request_with_no_cert_validation(
_ => None,
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
if is_const_false(&keyword.value) {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(verify_arg) = call_args.keyword_argument("verify") {
if is_const_false(verify_arg) {
checker.diagnostics.push(Diagnostic::new(
RequestWithNoCertValidation {
string: target.to_string(),
},
keyword.range(),
verify_arg.range(),
));
}
}

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs};
use crate::checkers::ast::Checker;
@@ -49,7 +49,12 @@ impl Violation for RequestWithoutTimeout {
}
/// S113
pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn request_without_timeout(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker
.semantic()
.resolve_call_path(func)
@@ -63,16 +68,12 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywor
)
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "timeout")
}) {
if is_const_none(&keyword.value) {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout) = call_args.keyword_argument("timeout") {
if is_const_none(timeout) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
keyword.range(),
timeout.range(),
));
}
} else {

View File

@@ -3,34 +3,10 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of SNMPv1 or SNMPv2.
///
/// ## Why is this bad?
/// The SNMPv1 and SNMPv2 protocols are considered insecure as they do
/// not support encryption. Instead, prefer SNMPv3, which supports
/// encryption.
///
/// ## Example
/// ```python
/// from pysnmp.hlapi import CommunityData
///
/// CommunityData("public", mpModel=0)
/// ```
///
/// Use instead:
/// ```python
/// from pysnmp.hlapi import CommunityData
///
/// CommunityData("public", mpModel=2)
/// ```
///
/// ## References
/// - [Cybersecurity and Infrastructure Security Agency (CISA): Alert TA17-156A](https://www.cisa.gov/news-events/alerts/2017/06/05/reducing-risk-snmp-abuse)
/// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html)
#[violation]
pub struct SnmpInsecureVersion;
@@ -42,7 +18,12 @@ impl Violation for SnmpInsecureVersion {
}
/// S508
pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn snmp_insecure_version(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker
.semantic()
.resolve_call_path(func)
@@ -50,21 +31,17 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"])
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "mpModel")
}) {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(mp_model_arg) = call_args.keyword_argument("mpModel") {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = &keyword.value
}) = &mp_model_arg
{
if value.is_zero() || value.is_one() {
checker
.diagnostics
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
.push(Diagnostic::new(SnmpInsecureVersion, mp_model_arg.range()));
}
}
}

View File

@@ -6,29 +6,6 @@ use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the SNMPv3 protocol without encryption.
///
/// ## Why is this bad?
/// Unencrypted SNMPv3 communication can be intercepted and read by
/// unauthorized parties. Instead, enable encryption when using SNMPv3.
///
/// ## Example
/// ```python
/// from pysnmp.hlapi import UsmUserData
///
/// UsmUserData("user")
/// ```
///
/// Use instead:
/// ```python
/// from pysnmp.hlapi import UsmUserData
///
/// UsmUserData("user", "authkey", "privkey")
/// ```
///
/// ## References
/// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html)
#[violation]
pub struct SnmpWeakCryptography;

View File

@@ -55,14 +55,16 @@ pub(crate) fn try_except_continue(
checker: &mut Checker,
except_handler: &ExceptHandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if matches!(body, [Stmt::Continue(_)]) {
if check_typed_exception || is_untyped_exception(type_, checker.semantic()) {
checker
.diagnostics
.push(Diagnostic::new(TryExceptContinue, except_handler.range()));
}
if body.len() == 1
&& body[0].is_continue_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(TryExceptContinue, except_handler.range()));
}
}

View File

@@ -51,14 +51,16 @@ pub(crate) fn try_except_pass(
checker: &mut Checker,
except_handler: &ExceptHandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if matches!(body, [Stmt::Pass(_)]) {
if check_typed_exception || is_untyped_exception(type_, checker.semantic()) {
checker
.diagnostics
.push(Diagnostic::new(TryExceptPass, except_handler.range()));
}
if body.len() == 1
&& body[0].is_pass_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(TryExceptPass, except_handler.range()));
}
}

View File

@@ -6,35 +6,6 @@ use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the `yaml.load` function.
///
/// ## Why is this bad?
/// Running the `yaml.load` function over untrusted YAML files is insecure, as
/// `yaml.load` allows for the creation of arbitrary Python objects, which can
/// then be used to execute arbitrary code.
///
/// Instead, consider using `yaml.safe_load`, which allows for the creation of
/// simple Python objects like integers and lists, but prohibits the creation of
/// more complex objects like functions and classes.
///
/// ## Example
/// ```python
/// import yaml
///
/// yaml.load(untrusted_yaml)
/// ```
///
/// Use instead:
/// ```python
/// import yaml
///
/// yaml.safe_load(untrusted_yaml)
/// ```
///
/// ## References
/// - [PyYAML documentation: Loading YAML](https://pyyaml.org/wiki/PyYAMLDocumentation)
/// - [Common Weakness Enumeration: CWE-20](https://cwe.mitre.org/data/definitions/20.html)
#[violation]
pub struct UnsafeYAMLLoad {
pub loader: Option<String>,

View File

@@ -11,11 +11,11 @@ S113.py:3:1: S113 Probable use of requests call without timeout
5 | requests.get('https://gmail.com', timeout=5)
|
S113.py:4:35: S113 Probable use of requests call with timeout set to `None`
S113.py:4:43: S113 Probable use of requests call with timeout set to `None`
|
3 | requests.get('https://gmail.com')
4 | requests.get('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
|
@@ -30,12 +30,12 @@ S113.py:6:1: S113 Probable use of requests call without timeout
8 | requests.post('https://gmail.com', timeout=5)
|
S113.py:7:36: S113 Probable use of requests call with timeout set to `None`
S113.py:7:44: S113 Probable use of requests call with timeout set to `None`
|
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
7 | requests.post('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
|
@@ -50,12 +50,12 @@ S113.py:9:1: S113 Probable use of requests call without timeout
11 | requests.put('https://gmail.com', timeout=5)
|
S113.py:10:35: S113 Probable use of requests call with timeout set to `None`
S113.py:10:43: S113 Probable use of requests call with timeout set to `None`
|
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
10 | requests.put('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
|
@@ -70,12 +70,12 @@ S113.py:12:1: S113 Probable use of requests call without timeout
14 | requests.delete('https://gmail.com', timeout=5)
|
S113.py:13:38: S113 Probable use of requests call with timeout set to `None`
S113.py:13:46: S113 Probable use of requests call with timeout set to `None`
|
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
13 | requests.delete('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
|
@@ -90,12 +90,12 @@ S113.py:15:1: S113 Probable use of requests call without timeout
17 | requests.patch('https://gmail.com', timeout=5)
|
S113.py:16:37: S113 Probable use of requests call with timeout set to `None`
S113.py:16:45: S113 Probable use of requests call with timeout set to `None`
|
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
16 | requests.patch('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
|
@@ -110,12 +110,12 @@ S113.py:18:1: S113 Probable use of requests call without timeout
20 | requests.options('https://gmail.com', timeout=5)
|
S113.py:19:39: S113 Probable use of requests call with timeout set to `None`
S113.py:19:47: S113 Probable use of requests call with timeout set to `None`
|
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
19 | requests.options('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
|
@@ -130,12 +130,12 @@ S113.py:21:1: S113 Probable use of requests call without timeout
23 | requests.head('https://gmail.com', timeout=5)
|
S113.py:22:36: S113 Probable use of requests call with timeout set to `None`
S113.py:22:44: S113 Probable use of requests call with timeout set to `None`
|
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
22 | requests.head('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
| ^^^^ S113
23 | requests.head('https://gmail.com', timeout=5)
|

View File

@@ -1,180 +1,180 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S501.py:5:47: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:5:54: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
4 | requests.get('https://gmail.com', timeout=30, verify=True)
5 | requests.get('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
6 | requests.post('https://gmail.com', timeout=30, verify=True)
7 | requests.post('https://gmail.com', timeout=30, verify=False)
|
S501.py:7:48: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:7:55: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
5 | requests.get('https://gmail.com', timeout=30, verify=False)
6 | requests.post('https://gmail.com', timeout=30, verify=True)
7 | requests.post('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
8 | requests.put('https://gmail.com', timeout=30, verify=True)
9 | requests.put('https://gmail.com', timeout=30, verify=False)
|
S501.py:9:47: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:9:54: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
7 | requests.post('https://gmail.com', timeout=30, verify=False)
8 | requests.put('https://gmail.com', timeout=30, verify=True)
9 | requests.put('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
10 | requests.delete('https://gmail.com', timeout=30, verify=True)
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
|
S501.py:11:50: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:11:57: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
9 | requests.put('https://gmail.com', timeout=30, verify=False)
10 | requests.delete('https://gmail.com', timeout=30, verify=True)
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
12 | requests.patch('https://gmail.com', timeout=30, verify=True)
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
|
S501.py:13:49: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:13:56: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
12 | requests.patch('https://gmail.com', timeout=30, verify=True)
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
14 | requests.options('https://gmail.com', timeout=30, verify=True)
15 | requests.options('https://gmail.com', timeout=30, verify=False)
|
S501.py:15:51: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:15:58: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
14 | requests.options('https://gmail.com', timeout=30, verify=True)
15 | requests.options('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
16 | requests.head('https://gmail.com', timeout=30, verify=True)
17 | requests.head('https://gmail.com', timeout=30, verify=False)
|
S501.py:17:48: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:17:55: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
15 | requests.options('https://gmail.com', timeout=30, verify=False)
16 | requests.head('https://gmail.com', timeout=30, verify=True)
17 | requests.head('https://gmail.com', timeout=30, verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
18 |
19 | httpx.request('GET', 'https://gmail.com', verify=True)
|
S501.py:20:43: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:20:50: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
19 | httpx.request('GET', 'https://gmail.com', verify=True)
20 | httpx.request('GET', 'https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
21 | httpx.get('https://gmail.com', verify=True)
22 | httpx.get('https://gmail.com', verify=False)
|
S501.py:22:32: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:22:39: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
20 | httpx.request('GET', 'https://gmail.com', verify=False)
21 | httpx.get('https://gmail.com', verify=True)
22 | httpx.get('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
23 | httpx.options('https://gmail.com', verify=True)
24 | httpx.options('https://gmail.com', verify=False)
|
S501.py:24:36: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:24:43: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
22 | httpx.get('https://gmail.com', verify=False)
23 | httpx.options('https://gmail.com', verify=True)
24 | httpx.options('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
25 | httpx.head('https://gmail.com', verify=True)
26 | httpx.head('https://gmail.com', verify=False)
|
S501.py:26:33: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:26:40: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
24 | httpx.options('https://gmail.com', verify=False)
25 | httpx.head('https://gmail.com', verify=True)
26 | httpx.head('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
27 | httpx.post('https://gmail.com', verify=True)
28 | httpx.post('https://gmail.com', verify=False)
|
S501.py:28:33: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:28:40: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
26 | httpx.head('https://gmail.com', verify=False)
27 | httpx.post('https://gmail.com', verify=True)
28 | httpx.post('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
29 | httpx.put('https://gmail.com', verify=True)
30 | httpx.put('https://gmail.com', verify=False)
|
S501.py:30:32: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:30:39: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
28 | httpx.post('https://gmail.com', verify=False)
29 | httpx.put('https://gmail.com', verify=True)
30 | httpx.put('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
31 | httpx.patch('https://gmail.com', verify=True)
32 | httpx.patch('https://gmail.com', verify=False)
|
S501.py:32:34: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:32:41: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
30 | httpx.put('https://gmail.com', verify=False)
31 | httpx.patch('https://gmail.com', verify=True)
32 | httpx.patch('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
33 | httpx.delete('https://gmail.com', verify=True)
34 | httpx.delete('https://gmail.com', verify=False)
|
S501.py:34:35: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:34:42: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
32 | httpx.patch('https://gmail.com', verify=False)
33 | httpx.delete('https://gmail.com', verify=True)
34 | httpx.delete('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
35 | httpx.stream('https://gmail.com', verify=True)
36 | httpx.stream('https://gmail.com', verify=False)
|
S501.py:36:35: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:36:42: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
34 | httpx.delete('https://gmail.com', verify=False)
35 | httpx.stream('https://gmail.com', verify=True)
36 | httpx.stream('https://gmail.com', verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
37 | httpx.Client()
38 | httpx.Client(verify=False)
|
S501.py:38:14: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:38:21: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
36 | httpx.stream('https://gmail.com', verify=False)
37 | httpx.Client()
38 | httpx.Client(verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
39 | httpx.AsyncClient()
40 | httpx.AsyncClient(verify=False)
|
S501.py:40:19: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:40:26: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
38 | httpx.Client(verify=False)
39 | httpx.AsyncClient()
40 | httpx.AsyncClient(verify=False)
| ^^^^^^^^^^^^ S501
| ^^^^^ S501
|

View File

@@ -1,20 +1,20 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S508.py:3:25: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
S508.py:3:33: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^ S508
| ^ S508
4 | CommunityData("public", mpModel=1) # S508
|
S508.py:4:25: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
S508.py:4:33: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^^^^^^^^^ S508
| ^ S508
5 |
6 | CommunityData("public", mpModel=2) # OK
|

View File

@@ -1,32 +1,32 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S701.py:9:57: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:9:68: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
7 | templateEnv = jinja2.Environment(autoescape=True,
8 | loader=templateLoader )
9 | Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
| ^^^^^^^^^^^^^^^^^^^^ S701
| ^^^^^^^^^ S701
10 | templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
11 | Environment(loader=templateLoader,
|
S701.py:10:34: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:10:45: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
8 | loader=templateLoader )
9 | Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
10 | templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
| ^^^^^^^^^^^^^^^^ S701
| ^^^^^ S701
11 | Environment(loader=templateLoader,
12 | load=templateLoader,
|
S701.py:13:13: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:13:24: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
11 | Environment(loader=templateLoader,
12 | load=templateLoader,
13 | autoescape=False) # S701
| ^^^^^^^^^^^^^^^^ S701
| ^^^^^ S701
14 |
15 | Environment(loader=templateLoader, # S701
|
@@ -40,12 +40,12 @@ S701.py:15:1: S701 By default, jinja2 sets `autoescape` to `False`. Consider usi
16 | load=templateLoader)
|
S701.py:29:36: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:29:47: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
27 | def fake_func():
28 | return 'foobar'
29 | Environment(loader=templateLoader, autoescape=fake_func()) # S701
| ^^^^^^^^^^^^^^^^^^^^^^ S701
| ^^^^^^^^^^^ S701
|

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -37,7 +38,12 @@ impl Violation for NoExplicitStacklevel {
}
/// B028
pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
pub(crate) fn no_explicit_stacklevel(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if !checker
.semantic()
.resolve_call_path(func)
@@ -48,12 +54,10 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keyword
return;
}
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "stacklevel")
}) {
if SimpleCallArgs::new(args, keywords)
.keyword_argument("stacklevel")
.is_some()
{
return;
}

View File

@@ -1,10 +1,9 @@
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `hasattr` to test if an object is callable (e.g.,
@@ -36,19 +35,13 @@ use crate::registry::AsRule;
pub struct UnreliableCallableCheck;
impl Violation for UnreliableCallableCheck {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Using `hasattr(x, \"__call__\")` to test if x is callable is unreliable. Use \
"Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
`callable(x)` for consistent results."
)
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `callable()`"))
}
}
/// B004
@@ -61,33 +54,23 @@ pub(crate) fn unreliable_callable_check(
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if !matches!(id.as_str(), "hasattr" | "getattr") {
if id != "getattr" && id != "hasattr" {
return;
}
let [obj, attr, ..] = args else {
if args.len() < 2 {
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
value: Constant::Str(s),
..
}) = attr
}) = &args[1]
else {
return;
};
if string != "__call__" {
if s != "__call__" {
return;
}
let mut diagnostic = Diagnostic::new(UnreliableCallableCheck, expr.range());
if checker.patch(diagnostic.kind.rule()) {
if id == "hasattr" {
if checker.semantic().is_builtin("callable") {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("callable({})", checker.locator.slice(obj.range())),
expr.range(),
)));
}
}
}
checker.diagnostics.push(diagnostic);
checker
.diagnostics
.push(Diagnostic::new(UnreliableCallableCheck, expr.range()));
}

View File

@@ -154,23 +154,20 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
},
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(rename) = rename {
if certainty.into() {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.filter(|binding| binding.range.start() >= expr.range().start())
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
rename,
expr.range(),
)));
}
if let Some(rename) = rename {
if certainty.into() && checker.patch(diagnostic.kind.rule()) {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
rename,
expr.range(),
)));
}
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B004.py:3:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
B004.py:3:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
1 | def this_is_a_bug():
2 | o = object()
@@ -10,18 +10,8 @@ B004.py:3:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is
4 | print("Ooh, callable! Or is it?")
5 | if getattr(o, "__call__", False):
|
= help: Replace with `callable()`
Fix
1 1 | def this_is_a_bug():
2 2 | o = object()
3 |- if hasattr(o, "__call__"):
3 |+ if callable(o):
4 4 | print("Ooh, callable! Or is it?")
5 5 | if getattr(o, "__call__", False):
6 6 | print("Ooh, callable! Or is it?")
B004.py:5:8: B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
B004.py:5:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
3 | if hasattr(o, "__call__"):
4 | print("Ooh, callable! Or is it?")
@@ -29,6 +19,5 @@ B004.py:5:8: B004 Using `hasattr(x, "__call__")` to test if x is callable is unr
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B004
6 | print("Ooh, callable! Or is it?")
|
= help: Replace with `callable()`

View File

@@ -72,42 +72,42 @@ B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaul
101 | ...
|
B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaults
B006_B008.py:218:20: B006 Do not use mutable data structures for argument defaults
|
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
216 | # B006 and B008
217 | # We should handle arbitrary nesting of these B008.
218 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
222 | pass
219 | pass
|
B006_B008.py:254:27: B006 Do not use mutable data structures for argument defaults
B006_B008.py:251:27: B006 Do not use mutable data structures for argument defaults
|
253 | def mutable_annotations(
254 | a: list[int] | None = [],
250 | def mutable_annotations(
251 | a: list[int] | None = [],
| ^^ B006
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
252 | b: Optional[Dict[int, int]] = {},
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
B006_B008.py:255:35: B006 Do not use mutable data structures for argument defaults
B006_B008.py:252:35: B006 Do not use mutable data structures for argument defaults
|
253 | def mutable_annotations(
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
250 | def mutable_annotations(
251 | a: list[int] | None = [],
252 | b: Optional[Dict[int, int]] = {},
| ^^ B006
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
257 | ):
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
254 | ):
|
B006_B008.py:256:62: B006 Do not use mutable data structures for argument defaults
B006_B008.py:253:62: B006 Do not use mutable data structures for argument defaults
|
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
251 | a: list[int] | None = [],
252 | b: Optional[Dict[int, int]] = {},
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
257 | ):
258 | pass
254 | ):
255 | pass
|

View File

@@ -46,38 +46,38 @@ B006_B008.py:120:30: B008 Do not perform function call in argument defaults
121 | ...
|
B006_B008.py:221:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:218:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
216 | # B006 and B008
217 | # We should handle arbitrary nesting of these B008.
218 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
222 | pass
219 | pass
|
B006_B008.py:227:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:224:22: B008 Do not perform function call `map` in argument defaults
|
225 | # Don't flag nested B006 since we can't guarantee that
226 | # it isn't made mutable by the outer operation.
227 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
222 | # Don't flag nested B006 since we can't guarantee that
223 | # it isn't made mutable by the outer operation.
224 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
228 | pass
225 | pass
|
B006_B008.py:232:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:229:19: B008 Do not perform function call `random.randint` in argument defaults
|
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
228 | # B008-ception.
229 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
233 | pass
230 | pass
|
B006_B008.py:232:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:229:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
228 | # B008-ception.
229 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
233 | pass
230 | pass
|

View File

@@ -1,5 +1,44 @@
use ruff_python_stdlib::builtins::is_builtin;
use ruff_text_size::TextRange;
use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt};
use ruff_python_ast::identifier::{Identifier, TryIdentifier};
use ruff_python_stdlib::builtins::BUILTINS;
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
is_builtin(name) && ignorelist.iter().all(|ignore| ignore != name)
BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum AnyShadowing<'a> {
Expression(&'a Expr),
Statement(&'a Stmt),
ExceptHandler(&'a ExceptHandler),
}
impl Identifier for AnyShadowing<'_> {
fn identifier(&self) -> TextRange {
match self {
AnyShadowing::Expression(expr) => expr.range(),
AnyShadowing::Statement(stmt) => stmt.identifier(),
AnyShadowing::ExceptHandler(handler) => handler.try_identifier().unwrap(),
}
}
}
impl<'a> From<&'a Stmt> for AnyShadowing<'a> {
fn from(value: &'a Stmt) -> Self {
AnyShadowing::Statement(value)
}
}
impl<'a> From<&'a Expr> for AnyShadowing<'a> {
fn from(value: &'a Expr) -> Self {
AnyShadowing::Expression(value)
}
}
impl<'a> From<&'a ExceptHandler> for AnyShadowing<'a> {
fn from(value: &'a ExceptHandler) -> Self {
AnyShadowing::ExceptHandler(value)
}
}

View File

@@ -1,12 +1,12 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use rustpython_parser::ast;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
use super::super::helpers::{shadows_builtin, AnyShadowing};
/// ## What it does
/// Checks for any class attributes that use the same name as a builtin.
@@ -67,7 +67,7 @@ pub(crate) fn builtin_attribute_shadowing(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
name: &str,
range: TextRange,
shadowing: AnyShadowing,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
@@ -84,7 +84,7 @@ pub(crate) fn builtin_attribute_shadowing(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
shadowing.identifier(),
));
}
}

View File

@@ -1,11 +1,11 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
use super::super::helpers::{shadows_builtin, AnyShadowing};
/// ## What it does
/// Checks for variable (and function) assignments that use the same name
@@ -59,13 +59,17 @@ impl Violation for BuiltinVariableShadowing {
}
/// A001
pub(crate) fn builtin_variable_shadowing(checker: &mut Checker, name: &str, range: TextRange) {
pub(crate) fn builtin_variable_shadowing(
checker: &mut Checker,
name: &str,
shadowing: AnyShadowing,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
checker.diagnostics.push(Diagnostic::new(
BuiltinVariableShadowing {
name: name.to_string(),
},
range,
shadowing.identifier(),
));
}
}

View File

@@ -1,28 +1,28 @@
---
source: crates/ruff/src/rules/flake8_builtins/mod.rs
---
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
A001.py:1:1: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^ A001
| ^^^^^^^^^^^^^^^^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
A001.py:2:1: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^ A001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
3 | from directory import new as dir
|
A001.py:3:30: A001 Variable `dir` is shadowing a Python builtin
A001.py:3:1: A001 Variable `dir` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
3 | from directory import new as dir
| ^^^ A001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
4 |
5 | print = 1
|

View File

@@ -1,19 +1,19 @@
---
source: crates/ruff/src/rules/flake8_builtins/mod.rs
---
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
A001.py:1:1: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^ A001
| ^^^^^^^^^^^^^^^^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
A001.py:2:1: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^ A001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
3 | from directory import new as dir
|

View File

@@ -1,18 +1,7 @@
//! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/).
use rustpython_parser::ast::{self, Expr};
pub(crate) mod rules;
pub mod settings;
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id)
} else {
false
}
}
#[cfg(test)]
mod tests {
use std::path::Path;

View File

@@ -5,40 +5,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for f-strings in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with an f-string as its argument can cause unexpected
/// behavior. Since the f-string is resolved before the function call, the
/// translation catalog will look up the formatted string, rather than the
/// f-string template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _(f"Hello, {name}!") # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct FStringInGetTextFuncCall;

View File

@@ -5,40 +5,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `str.format` calls in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with a formatted string as its argument can cause
/// unexpected behavior. Since the formatted string is resolved before the
/// function call, the translation catalog will look up the formatted string,
/// rather than the `str.format`-style template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!" % name) # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct FormatInGetTextFuncCall;

View File

@@ -0,0 +1,10 @@
use rustpython_parser::ast::{self, Expr};
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id)
} else {
false
}
}

View File

@@ -1,7 +1,9 @@
pub(crate) use f_string_in_gettext_func_call::*;
pub(crate) use format_in_gettext_func_call::*;
pub(crate) use is_gettext_func_call::*;
pub(crate) use printf_in_gettext_func_call::*;
mod f_string_in_gettext_func_call;
mod format_in_gettext_func_call;
mod is_gettext_func_call;
mod printf_in_gettext_func_call;

View File

@@ -4,40 +4,6 @@ use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for printf-style formatted strings in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with a formatted string as its argument can cause
/// unexpected behavior. Since the formatted string is resolved before the
/// function call, the translation catalog will look up the formatted string,
/// rather than the printf-style template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, {}!".format(name)) # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct PrintfInGetTextFuncCall;

View File

@@ -208,7 +208,12 @@ fn check_positional_args(
(ErrorKind::ThirdArgBadAnnotation, is_traceback_type),
];
for (arg, (error_info, predicate)) in positional_args.iter().skip(1).take(3).zip(validations) {
for (arg, (error_info, predicate)) in positional_args
.iter()
.skip(1)
.take(3)
.zip(validations.into_iter())
{
let Some(annotation) = arg.def.annotation.as_ref() else {
continue;
};

View File

@@ -11,7 +11,7 @@ use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged,
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::helpers::{has_comments_in, Truthiness};
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{visitor, whitespace};
@@ -197,7 +197,7 @@ pub(crate) fn unittest_assertion(
if checker.semantic().stmt().is_expr_stmt()
&& checker.semantic().expr_parent().is_none()
&& !checker.semantic().scope().kind.is_lambda()
&& !checker.indexer.comment_ranges().intersects(expr.range())
&& !has_comments_in(expr.range(), checker.locator)
{
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
@@ -483,7 +483,7 @@ pub(crate) fn composite_condition(
if checker.patch(diagnostic.kind.rule()) {
if matches!(composite, CompositionKind::Simple)
&& msg.is_none()
&& !checker.indexer.comment_ranges().intersects(stmt.range())
&& !has_comments_in(stmt.range(), checker.locator)
{
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {

View File

@@ -505,7 +505,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
}
// Avoid removing comments.
if has_comments(expr, checker.locator, checker.indexer) {
if has_comments(expr, checker.locator) {
continue;
}

View File

@@ -6,7 +6,9 @@ use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifie
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
use ruff_python_ast::helpers::{any_over_expr, contains_effect, first_colon_range, has_comments};
use ruff_python_ast::helpers::{
any_over_expr, contains_effect, first_colon_range, has_comments, has_comments_in,
};
use ruff_python_semantic::SemanticModel;
use ruff_python_whitespace::UniversalNewlines;
@@ -376,11 +378,10 @@ pub(crate) fn nested_if_statements(
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
let nested_if = &body[0];
if !checker
.indexer
.comment_ranges()
.intersects(TextRange::new(stmt.start(), nested_if.start()))
{
if !has_comments_in(
TextRange::new(stmt.start(), nested_if.start()),
checker.locator,
) {
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
Ok(edit) => {
if edit
@@ -463,7 +464,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
if checker.patch(diagnostic.kind.rule()) {
if matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !has_comments(stmt, checker.locator, checker.indexer)
&& !has_comments(stmt, checker.locator)
&& (test.is_compare_expr() || checker.semantic().is_builtin("bool"))
{
if test.is_compare_expr() {
@@ -649,7 +650,7 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator, checker.indexer) {
if !has_comments(stmt, checker.locator) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
stmt.range(),
@@ -1049,7 +1050,7 @@ pub(crate) fn use_dict_get_with_default(
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator, checker.indexer) {
if !has_comments(stmt, checker.locator) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
stmt.range(),

View File

@@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Ranged, Stmt, WithItem};
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::first_colon_range;
use ruff_python_ast::helpers::{first_colon_range, has_comments_in};
use ruff_python_whitespace::UniversalNewlines;
use crate::checkers::ast::Checker;
@@ -129,11 +129,10 @@ pub(crate) fn multiple_with_statements(
),
);
if checker.patch(diagnostic.kind.rule()) {
if !checker
.indexer
.comment_ranges()
.intersects(TextRange::new(with_stmt.start(), with_body[0].start()))
{
if !has_comments_in(
TextRange::new(with_stmt.start(), with_body[0].start()),
checker.locator,
) {
match fix_with::fix_multiple_with_statements(
checker.locator,
checker.stylist,

View File

@@ -117,7 +117,7 @@ pub(crate) fn suppressible_exception(
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator, checker.indexer) {
if !has_comments(stmt, checker.locator) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer.get_or_import_symbol(
&ImportRequest::import("contextlib", "suppress"),

View File

@@ -15,10 +15,13 @@ mod tests {
use crate::test::{test_path, test_snippet};
use crate::{assert_messages, settings};
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_1.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_2.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_3.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_4.py"))]
@@ -27,13 +30,12 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
#[test_case(Rule::UnquotedAnnotation, Path::new("TCH200_0.py"))]
#[test_case(Rule::UnquotedAnnotation, Path::new("TCH200_1.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,7 +1,9 @@
pub(crate) use empty_type_checking_block::*;
pub(crate) use quoted_annotation::*;
pub(crate) use runtime_import_in_type_checking_block::*;
pub(crate) use typing_only_runtime_import::*;
mod empty_type_checking_block;
mod quoted_annotation;
mod runtime_import_in_type_checking_block;
mod typing_only_runtime_import;

View File

@@ -0,0 +1,100 @@
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::BindingId;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for the presence of unnecessary quotes in type annotations.
///
/// ## Why is this bad?
/// In Python, type annotations can be quoted to avoid forward references.
/// However, if `from __future__ import annotations` is present, Python
/// will always evaluate type annotations in a deferred manner, making
/// the quotes unnecessary.
///
/// ## Example
/// ```python
/// from __future__ import annotations
///
///
/// def foo(bar: "Bar") -> "Bar":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from __future__ import annotations
///
///
/// def foo(bar: Bar) -> Bar:
/// ...
/// ```
///
/// ## References
/// - [PEP 563](https://peps.python.org/pep-0563/)
/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__)
#[violation]
pub struct UnquotedAnnotation {
name: String,
}
impl Violation for UnquotedAnnotation {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let UnquotedAnnotation { name } = self;
format!("Typing-only variable referenced in runtime annotation: `{name}`")
}
fn autofix_title(&self) -> Option<String> {
Some("Add quotes".to_string())
}
}
/// TCH200
pub(crate) fn unquoted_annotation(checker: &mut Checker, binding_id: BindingId, expr: &Expr) {
// If we're already in a quoted annotation, skip.
if checker.semantic().in_deferred_type_definition() {
return;
}
// If we're in a typing-only context, skip.
if checker.semantic().execution_context().is_typing() {
return;
}
// If the reference resolved to a typing-only import, flag.
if checker.semantic().bindings[binding_id].context.is_typing() {
// Expand any attribute chains (e.g., flag `typing.List` in `typing.List[int]`).
let mut expr = expr;
for parent in checker.semantic().expr_ancestors() {
if parent.is_attribute_expr() {
expr = parent;
} else {
break;
}
}
let mut diagnostic = Diagnostic::new(
UnquotedAnnotation {
name: checker.locator.slice(expr.range()).to_string(),
},
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// We can only _fix_ this if we're in a type annotation.
if checker.semantic().in_runtime_annotation() {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("\"{}\"", checker.locator.slice(expr.range()).to_string()),
expr.range(),
)));
}
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -0,0 +1,92 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
---
TCH200_0.py:8:12: TCH200 [*] Typing-only variable referenced in runtime annotation: `Class`
|
8 | def f(var: Class) -> Class:
| ^^^^^ TCH200
9 | x: Class
|
= help: Add quotes
Fix
5 5 | from module import Class
6 6 |
7 7 |
8 |-def f(var: Class) -> Class:
8 |+def f(var: "Class") -> Class:
9 9 | x: Class
10 10 |
11 11 |
TCH200_0.py:8:22: TCH200 [*] Typing-only variable referenced in runtime annotation: `Class`
|
8 | def f(var: Class) -> Class:
| ^^^^^ TCH200
9 | x: Class
|
= help: Add quotes
Fix
5 5 | from module import Class
6 6 |
7 7 |
8 |-def f(var: Class) -> Class:
8 |+def f(var: Class) -> "Class":
9 9 | x: Class
10 10 |
11 11 |
TCH200_0.py:12:12: TCH200 [*] Typing-only variable referenced in runtime annotation: `module.Class`
|
12 | def f(var: module.Class) -> module.Class:
| ^^^^^^^^^^^^ TCH200
13 | x: module.Class
|
= help: Add quotes
Fix
9 9 | x: Class
10 10 |
11 11 |
12 |-def f(var: module.Class) -> module.Class:
12 |+def f(var: "module.Class") -> module.Class:
13 13 | x: module.Class
14 14 |
15 15 |
TCH200_0.py:12:29: TCH200 [*] Typing-only variable referenced in runtime annotation: `module.Class`
|
12 | def f(var: module.Class) -> module.Class:
| ^^^^^^^^^^^^ TCH200
13 | x: module.Class
|
= help: Add quotes
Fix
9 9 | x: Class
10 10 |
11 11 |
12 |-def f(var: module.Class) -> module.Class:
12 |+def f(var: module.Class) -> "module.Class":
13 13 | x: module.Class
14 14 |
15 15 |
TCH200_0.py:17:11: TCH200 Typing-only variable referenced in runtime annotation: `Class`
|
16 | def f():
17 | print(Class)
| ^^^^^ TCH200
|
= help: Add quotes
TCH200_0.py:21:11: TCH200 Typing-only variable referenced in runtime annotation: `module.Class`
|
20 | def f():
21 | print(module.Class)
| ^^^^^^^^^^^^ TCH200
|
= help: Add quotes

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
---

View File

@@ -777,35 +777,6 @@ mod tests {
Ok(())
}
#[test_case(Path::new("comment.py"))]
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]
#[test_case(Path::new("empty.py"))]
#[test_case(Path::new("existing_import.py"))]
#[test_case(Path::new("multiline_docstring.py"))]
#[test_case(Path::new("off.py"))]
fn required_import_with_alias(path: &Path) -> Result<()> {
let snapshot = format!("required_import_with_alias_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort/required_imports").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
required_imports: BTreeSet::from([
"from __future__ import annotations as _annotations".to_string(),
]),
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::MissingRequiredImport)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_only.py"))]

View File

@@ -1,19 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
comment.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | #!/usr/bin/env python3
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | #!/usr/bin/env python3
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1

View File

@@ -1,19 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | """Hello, world!"""
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -1,17 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring_with_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""; x = \
| I002
2 | 1; y = 2
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |-"""Hello, world!"""; x = \
1 |+"""Hello, world!"""; from __future__ import annotations as _annotations; x = \
2 2 | 1; y = 2

View File

@@ -1,15 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring_with_semicolon.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""; x = 1
| I002
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |-"""Hello, world!"""; x = 1
1 |+"""Hello, world!"""; from __future__ import annotations as _annotations; x = 1

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -1,17 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
existing_import.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | from __future__ import generator_stop
| I002
2 | import os
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |+from __future__ import annotations as _annotations
1 2 | from __future__ import generator_stop
2 3 | import os

View File

@@ -1,20 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
multiline_docstring.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """a
| I002
2 | b"""
3 | # b
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | """a
2 2 | b"""
3 3 | # b
4 |+from __future__ import annotations as _annotations
4 5 | import os

View File

@@ -1,20 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
off.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | # isort: off
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | # isort: off
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1
4 5 | # isort: on

View File

@@ -237,6 +237,27 @@ mod tests {
"#,
"PD011_pass_node_name"
)]
#[test_case(
r#"
import pandas as pd
employees = pd.read_csv(input_file)
"#,
"PD012_pass_read_csv"
)]
#[test_case(
r#"
import pandas as pd
employees = pd.read_table(input_file)
"#,
"PD012_fail_read_table"
)]
#[test_case(
r#"
import pandas as pd
employees = read_table
"#,
"PD012_node_Name_pass"
)]
#[test_case(
r#"
import pandas as pd
@@ -339,12 +360,7 @@ mod tests {
assert_messages!(snapshot, diagnostics);
}
#[test_case(
Rule::PandasUseOfDotReadTable,
Path::new("pandas_use_of_dot_read_table.py")
)]
#[test_case(Rule::PandasUseOfInplaceArgument, Path::new("PD002.py"))]
#[test_case(Rule::PandasNuniqueConstantSeriesCheck, Path::new("PD101.py"))]
fn paths(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -126,6 +126,18 @@ impl Violation for PandasUseOfDotPivotOrUnstack {
}
}
// TODO(tjkuson): Add documentation for this rule once clarified.
// https://github.com/astral-sh/ruff/issues/5628
#[violation]
pub struct PandasUseOfDotReadTable;
impl Violation for PandasUseOfDotReadTable {
#[derive_message_formats]
fn message(&self) -> String {
format!("`.read_csv` is preferred to `.read_table`; provides same functionality")
}
}
/// ## What it does
/// Checks for uses of `.stack` on Pandas objects.
///
@@ -181,6 +193,14 @@ pub(crate) fn call(checker: &mut Checker, func: &Expr) {
{
PandasUseOfDotPivotOrUnstack.into()
}
"read_table"
if checker
.settings
.rules
.enabled(Rule::PandasUseOfDotReadTable) =>
{
PandasUseOfDotReadTable.into()
}
"stack" if checker.settings.rules.enabled(Rule::PandasUseOfDotStack) => {
PandasUseOfDotStack.into()
}

View File

@@ -2,16 +2,12 @@ pub(crate) use assignment_to_df::*;
pub(crate) use attr::*;
pub(crate) use call::*;
pub(crate) use inplace_argument::*;
pub(crate) use nunique_constant_series_check::*;
pub(crate) use pd_merge::*;
pub(crate) use read_table::*;
pub(crate) use subscript::*;
pub(crate) mod assignment_to_df;
pub(crate) mod attr;
pub(crate) mod call;
pub(crate) mod inplace_argument;
pub(crate) mod nunique_constant_series_check;
pub(crate) mod pd_merge;
pub(crate) mod read_table;
pub(crate) mod subscript;

View File

@@ -1,122 +0,0 @@
use num_traits::One;
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::rules::pandas_vet::helpers::{test_expression, Resolution};
/// ## What it does
/// Check for uses of `.nunique()` to check if a Pandas Series is constant
/// (i.e., contains only one unique value).
///
/// ## Why is this bad?
/// `.nunique()` is computationally inefficient for checking if a Series is
/// constant.
///
/// Consider, for example, a Series of length `n` that consists of increasing
/// integer values (e.g., 1, 2, 3, 4). The `.nunique()` method will iterate
/// over the entire Series to count the number of unique values. But in this
/// case, we can detect that the Series is non-constant after visiting the
/// first two values, which are non-equal.
///
/// In general, `.nunique()` requires iterating over the entire Series, while a
/// more efficient approach allows short-circuiting the operation as soon as a
/// non-equal value is found.
///
/// Instead of calling `.nunique()`, convert the Series to a NumPy array, and
/// check if all values in the array are equal to the first observed value.
///
/// ## Example
/// ```python
/// import pandas as pd
///
/// data = pd.Series(range(1000))
/// if data.nunique() <= 1:
/// print("Series is constant")
/// ```
///
/// Use instead:
/// ```python
/// import pandas as pd
///
/// data = pd.Series(range(1000))
/// array = data.to_numpy()
/// if array.shape[0] == 0 or (array[0] == array).all():
/// print("Series is constant")
/// ```
///
/// ## References
/// - [Pandas Cookbook: "Constant Series"](https://pandas.pydata.org/docs/user_guide/cookbook.html#constant-series)
/// - [Pandas documentation: `nunique`](https://pandas.pydata.org/docs/reference/api/pandas.Series.nunique.html)
#[violation]
pub struct PandasNuniqueConstantSeriesCheck;
impl Violation for PandasNuniqueConstantSeriesCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `series.nunique()` for checking that a series is constant is inefficient")
}
}
/// PD101
pub(crate) fn nunique_constant_series_check(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[CmpOp],
comparators: &[Expr],
) {
let ([op], [right]) = (ops, comparators) else {
return;
};
// Operators may be ==, !=, <=, >.
if !matches!(op, CmpOp::Eq | CmpOp::NotEq | CmpOp::LtE | CmpOp::Gt,) {
return;
}
// Right should be the integer 1.
if !is_constant_one(right) {
return;
}
// Check if call is `.nuniuqe()`.
let Expr::Call(ast::ExprCall { func, .. }) = left else {
return;
};
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
return;
};
if attr.as_str() != "nunique" {
return;
}
// Avoid flagging on non-Series (e.g., `{"a": 1}.at[0]`).
if !matches!(
test_expression(value, checker.semantic()),
Resolution::RelevantLocal
) {
return;
}
checker.diagnostics.push(Diagnostic::new(
PandasNuniqueConstantSeriesCheck,
expr.range(),
));
}
/// Return `true` if an [`Expr`] is a constant `1`.
fn is_constant_one(expr: &Expr) -> bool {
match expr {
Expr::Constant(constant) => match &constant.value {
Constant::Int(int) => int.is_one(),
_ => false,
},
_ => false,
}
}

View File

@@ -1,76 +0,0 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `pd.read_table` to read CSV files.
///
/// ## Why is this bad?
/// In the Pandas API, `pd.read_csv` and `pd.read_table` are equivalent apart
/// from their default separator: `pd.read_csv` defaults to a comma (`,`),
/// while `pd.read_table` defaults to a tab (`\t`) as the default separator.
///
/// Prefer `pd.read_csv` over `pd.read_table` when reading comma-separated
/// data (like CSV files), as it is more idiomatic.
///
/// ## Example
/// ```python
/// import pandas as pd
///
/// cities_df = pd.read_table("cities.csv", sep=",")
/// ```
///
/// Use instead:
/// ```python
/// import pandas as pd
///
/// cities_df = pd.read_csv("cities.csv")
/// ```
///
/// ## References
/// - [Pandas documentation: `read_csv`](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html#pandas.read_csv)
/// - [Pandas documentation: `read_table`](https://pandas.pydata.org/docs/reference/api/pandas.read_table.html#pandas.read_table)
#[violation]
pub struct PandasUseOfDotReadTable;
impl Violation for PandasUseOfDotReadTable {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `.read_csv` instead of `.read_table` to read CSV files")
}
}
/// PD012
pub(crate) fn use_of_read_table(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["pandas", "read_table"])
})
{
if let Some(Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
})) = keywords
.iter()
.find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |keyword| keyword.as_str() == "sep")
})
.map(|keyword| &keyword.value)
{
if value.as_str() == "," {
checker
.diagnostics
.push(Diagnostic::new(PandasUseOfDotReadTable, func.range()));
}
}
}
}

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
<filename>:3:13: PD012 `.read_csv` is preferred to `.read_table`; provides same functionality
|
2 | import pandas as pd
3 | employees = pd.read_table(input_file)
| ^^^^^^^^^^^^^ PD012
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---

View File

@@ -1,42 +0,0 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
pandas_use_of_dot_read_table.py:4:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
3 | # Errors.
4 | df = pd.read_table("data.csv", sep=",")
| ^^^^^^^^^^^^^ PD012
5 | df = pd.read_table("data.csv", sep=",", header=0)
6 | filename = "data.csv"
|
pandas_use_of_dot_read_table.py:5:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
3 | # Errors.
4 | df = pd.read_table("data.csv", sep=",")
5 | df = pd.read_table("data.csv", sep=",", header=0)
| ^^^^^^^^^^^^^ PD012
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
|
pandas_use_of_dot_read_table.py:7:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
5 | df = pd.read_table("data.csv", sep=",", header=0)
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
| ^^^^^^^^^^^^^ PD012
8 | df = pd.read_table(filename, sep=",", header=0)
|
pandas_use_of_dot_read_table.py:8:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
8 | df = pd.read_table(filename, sep=",", header=0)
| ^^^^^^^^^^^^^ PD012
9 |
10 | # Non-errors.
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---

View File

@@ -1,122 +0,0 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
PD101.py:7:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
6 | # PD101
7 | data.nunique() <= 1
| ^^^^^^^^^^^^^^^^^^^ PD101
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
|
PD101.py:8:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
6 | # PD101
7 | data.nunique() <= 1
8 | data.nunique(dropna=True) <= 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
|
PD101.py:9:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
7 | data.nunique() <= 1
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
|
PD101.py:10:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
| ^^^^^^^^^^^^^^^^^^^ PD101
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
|
PD101.py:11:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
|
PD101.py:12:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
|
PD101.py:13:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
| ^^^^^^^^^^^^^^^^^^^ PD101
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
|
PD101.py:14:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
|
PD101.py:15:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
|
PD101.py:16:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
| ^^^^^^^^^^^^^^^^^^ PD101
17 | data.dropna().nunique() == 1
18 | data[data.notnull()].nunique() == 1
|
PD101.py:17:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
18 | data[data.notnull()].nunique() == 1
|
PD101.py:18:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
18 | data[data.notnull()].nunique() == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
19 |
20 | # No violation of this rule
|

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -53,6 +53,7 @@ impl Violation for MixedCaseVariableInClassScope {
pub(crate) fn mixed_case_variable_in_class_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
bases: &[Expr],
) {
@@ -65,22 +66,15 @@ pub(crate) fn mixed_case_variable_in_class_scope(
{
return;
}
if !helpers::is_mixed_case(name) {
return;
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_class(bases, checker.semantic())
if helpers::is_mixed_case(name)
&& !helpers::is_named_tuple_assignment(stmt, checker.semantic())
&& !helpers::is_typed_dict_class(bases, checker.semantic())
{
return;
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInClassScope {
name: name.to_string(),
},
expr.range(),
));
}
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInClassScope {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -60,7 +60,12 @@ impl Violation for MixedCaseVariableInGlobalScope {
}
/// N816
pub(crate) fn mixed_case_variable_in_global_scope(checker: &mut Checker, expr: &Expr, name: &str) {
pub(crate) fn mixed_case_variable_in_global_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if checker
.settings
.pep8_naming
@@ -70,20 +75,13 @@ pub(crate) fn mixed_case_variable_in_global_scope(checker: &mut Checker, expr: &
{
return;
}
if !helpers::is_mixed_case(name) {
return;
if helpers::is_mixed_case(name) && !helpers::is_named_tuple_assignment(stmt, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInGlobalScope {
name: name.to_string(),
},
expr.range(),
));
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic()) {
return;
}
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInGlobalScope {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -50,7 +50,12 @@ impl Violation for NonLowercaseVariableInFunction {
}
/// N806
pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &Expr, name: &str) {
pub(crate) fn non_lowercase_variable_in_function(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if checker
.settings
.pep8_naming
@@ -61,22 +66,16 @@ pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &E
return;
}
if str::is_lowercase(name) {
return;
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_assignment(parent, checker.semantic())
|| helpers::is_type_var_assignment(parent, checker.semantic())
if !str::is_lowercase(name)
&& !helpers::is_named_tuple_assignment(stmt, checker.semantic())
&& !helpers::is_typed_dict_assignment(stmt, checker.semantic())
&& !helpers::is_type_var_assignment(stmt, checker.semantic())
{
return;
checker.diagnostics.push(Diagnostic::new(
NonLowercaseVariableInFunction {
name: name.to_string(),
},
expr.range(),
));
}
checker.diagnostics.push(Diagnostic::new(
NonLowercaseVariableInFunction {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,12 +1,12 @@
use std::fmt;
use regex::Regex;
use rustpython_parser::ast;
use rustpython_parser::ast::Expr;
use rustpython_parser::ast::Ranged;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -28,16 +28,16 @@ use crate::registry::AsRule;
///
/// ## Example
/// ```python
/// obj = {"a": 1, "b": 2}
/// for key, value in obj.items():
/// print(value)
/// some_dict = {"a": 1, "b": 2}
/// for _, val in some_dict.items():
/// print(val)
/// ```
///
/// Use instead:
/// ```python
/// obj = {"a": 1, "b": 2}
/// for value in obj.values():
/// print(value)
/// some_dict = {"a": 1, "b": 2}
/// for val in some_dict.values():
/// print(val)
/// ```
#[violation]
pub struct IncorrectDictIterator {
@@ -79,8 +79,8 @@ pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter
}
match (
is_unused(key, checker.semantic()),
is_unused(value, checker.semantic()),
is_ignored_tuple_or_name(key, &checker.settings.dummy_variable_rgx),
is_ignored_tuple_or_name(value, &checker.settings.dummy_variable_rgx),
) {
(true, true) => {
// Both the key and the value are unused.
@@ -142,33 +142,13 @@ impl fmt::Display for DictSubset {
}
}
/// Returns `true` if the given expression is either an unused value or a tuple of unused values.
fn is_unused(expr: &Expr, model: &SemanticModel) -> bool {
/// Returns `true` if the given expression is either an ignored value or a tuple of ignored values.
fn is_ignored_tuple_or_name(expr: &Expr, dummy_variable_rgx: &Regex) -> bool {
match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(|expr| is_unused(expr, model)),
Expr::Name(ast::ExprName { id, .. }) => {
// Treat a variable as used if it has any usages, _or_ it's shadowed by another variable
// with usages.
//
// If we don't respect shadowing, we'll incorrectly flag `bar` as unused in:
// ```python
// from random import random
//
// for bar in range(10):
// if random() > 0.5:
// break
// else:
// bar = 1
//
// print(bar)
// ```
let scope = model.scope();
scope
.get_all(id)
.map(|binding_id| model.binding(binding_id))
.filter(|binding| binding.range.start() >= expr.range().start())
.all(|binding| !binding.is_used())
}
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.all(|expr| is_ignored_tuple_or_name(expr, dummy_variable_rgx)),
Expr::Name(ast::ExprName { id, .. }) => dummy_variable_rgx.is_match(id.as_str()),
_ => false,
}
}

View File

@@ -1,211 +1,167 @@
---
source: crates/ruff/src/rules/perflint/mod.rs
---
PERF102.py:5:21: PERF102 [*] When using only the values of a dict use the `values()` method
PERF102.py:3:17: PERF102 [*] When using only the values of a dict use the `values()` method
|
4 | def f():
5 | for _, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
6 | print(value)
1 | some_dict = {"a": 12, "b": 32, "c": 44}
2 |
3 | for _, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
4 | print(value)
|
= help: Replace `.items()` with `.values()`
Suggested fix
1 1 | some_dict = {"a": 12, "b": 32, "c": 44}
2 2 |
3 3 |
4 4 | def f():
5 |- for _, value in some_dict.items(): # PERF102
5 |+ for value in some_dict.values(): # PERF102
6 6 | print(value)
7 7 |
8 8 |
3 |-for _, value in some_dict.items(): # PERF102
3 |+for value in some_dict.values(): # PERF102
4 4 | print(value)
5 5 |
6 6 |
PERF102.py:10:19: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:7:15: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
7 | for key, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
8 | print(key)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
4 4 | print(value)
5 5 |
6 6 |
7 |-for key, _ in some_dict.items(): # PERF102
7 |+for key in some_dict.keys(): # PERF102
8 8 | print(key)
9 9 |
10 10 |
PERF102.py:11:26: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
9 | def f():
10 | for key, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
11 | print(key)
11 | for weird_arg_name, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
12 | print(weird_arg_name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
7 7 |
8 8 |
9 9 | def f():
10 |- for key, _ in some_dict.items(): # PERF102
10 |+ for key in some_dict.keys(): # PERF102
11 11 | print(key)
12 12 |
8 8 | print(key)
9 9 |
10 10 |
11 |-for weird_arg_name, _ in some_dict.items(): # PERF102
11 |+for weird_arg_name in some_dict.keys(): # PERF102
12 12 | print(weird_arg_name)
13 13 |
14 14 |
PERF102.py:15:30: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:15:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
14 | def f():
15 | for weird_arg_name, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
16 | print(weird_arg_name)
15 | for name, (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
16 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
12 12 |
12 12 | print(weird_arg_name)
13 13 |
14 14 | def f():
15 |- for weird_arg_name, _ in some_dict.items(): # PERF102
15 |+ for weird_arg_name in some_dict.keys(): # PERF102
16 16 | print(weird_arg_name)
14 14 |
15 |-for name, (_, _) in some_dict.items(): # PERF102
15 |+for name in some_dict.keys(): # PERF102
16 16 | pass
17 17 |
18 18 |
PERF102.py:20:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:23:26: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
19 | def f():
20 | for name, (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
21 | print(name)
23 | for (key1, _), (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
24 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
17 17 |
18 18 |
19 19 | def f():
20 |- for name, (_, _) in some_dict.items(): # PERF102
20 |+ for name in some_dict.keys(): # PERF102
21 21 | print(name)
20 20 | pass
21 21 |
22 22 |
23 23 |
23 |-for (key1, _), (_, _) in some_dict.items(): # PERF102
23 |+for (key1, _) in some_dict.keys(): # PERF102
24 24 | pass
25 25 |
26 26 |
PERF102.py:30:30: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:27:32: PERF102 [*] When using only the values of a dict use the `values()` method
|
29 | def f():
30 | for (key1, _), (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
31 | print(key1)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
27 27 |
28 28 |
29 29 | def f():
30 |- for (key1, _), (_, _) in some_dict.items(): # PERF102
30 |+ for (key1, _) in some_dict.keys(): # PERF102
31 31 | print(key1)
32 32 |
33 33 |
PERF102.py:35:36: PERF102 [*] When using only the values of a dict use the `values()` method
|
34 | def f():
35 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
36 | print(value)
27 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
28 | pass
|
= help: Replace `.items()` with `.values()`
Suggested fix
32 32 |
33 33 |
34 34 | def f():
35 |- for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
35 |+ for (value, _) in some_dict.values(): # PERF102
36 36 | print(value)
24 24 | pass
25 25 |
26 26 |
27 |-for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
27 |+for (value, _) in some_dict.values(): # PERF102
28 28 | pass
29 29 |
30 30 |
PERF102.py:39:28: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
39 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
40 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
36 36 | pass
37 37 |
38 38 |
39 |-for ((_, key2), (_, _)) in some_dict.items(): # PERF102
39 |+for (_, key2) in some_dict.keys(): # PERF102
40 40 | pass
41 41 |
42 42 |
PERF102.py:50:32: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:67:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
49 | def f():
50 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
51 | print(key2)
67 | for name, (_, _) in (some_function()).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^ PERF102
68 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
47 47 |
48 48 |
49 49 | def f():
50 |- for ((_, key2), (_, _)) in some_dict.items(): # PERF102
50 |+ for (_, key2) in some_dict.keys(): # PERF102
51 51 | print(key2)
52 52 |
53 53 |
64 64 | print(value)
65 65 |
66 66 |
67 |-for name, (_, _) in (some_function()).items(): # PERF102
67 |+for name in (some_function()).keys(): # PERF102
68 68 | pass
69 69 |
70 70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
PERF102.py:85:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:70:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
84 | def f():
85 | for name, (_, _) in (some_function()).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^ PERF102
86 | print(name)
68 | pass
69 |
70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102
71 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
82 82 |
83 83 |
84 84 | def f():
85 |- for name, (_, _) in (some_function()).items(): # PERF102
85 |+ for name in (some_function()).keys(): # PERF102
86 86 | print(name)
87 87 |
88 88 |
PERF102.py:90:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
89 | def f():
90 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102
91 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
87 87 |
88 88 |
89 89 | def f():
90 |- for name, (_, _) in (some_function().some_attribute).items(): # PERF102
90 |+ for name in (some_function().some_attribute).keys(): # PERF102
91 91 | print(name)
92 92 |
93 93 |
PERF102.py:95:31: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
94 | def f():
95 | for name, unused_value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
96 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
92 92 |
93 93 |
94 94 | def f():
95 |- for name, unused_value in some_dict.items(): # PERF102
95 |+ for name in some_dict.keys(): # PERF102
96 96 | print(name)
97 97 |
98 98 |
PERF102.py:100:31: PERF102 [*] When using only the values of a dict use the `values()` method
|
99 | def f():
100 | for unused_name, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
101 | print(value)
|
= help: Replace `.items()` with `.values()`
Suggested fix
97 97 |
98 98 |
99 99 | def f():
100 |- for unused_name, value in some_dict.items(): # PERF102
100 |+ for value in some_dict.values(): # PERF102
101 101 | print(value)
67 67 | for name, (_, _) in (some_function()).items(): # PERF102
68 68 | pass
69 69 |
70 |-for name, (_, _) in (some_function().some_attribute).items(): # PERF102
70 |+for name in (some_function().some_attribute).keys(): # PERF102
71 71 | pass

View File

@@ -53,13 +53,16 @@ pub(super) fn is_overlong(
task_tags: &[String],
tab_size: TabSize,
) -> Option<Overlong> {
// Each character is between 1-4 bytes. If the number of bytes is smaller than the limit, it cannot be overlong.
if line.len() < limit.get() {
return None;
let mut start_offset = line.start();
let mut width = LineWidth::new(tab_size);
for c in line.chars() {
if width < limit {
start_offset += c.text_len();
}
width = width.add_char(c);
}
let mut width = LineWidth::new(tab_size);
width = width.add_str(line.as_str());
if width <= limit {
return None;
}
@@ -88,17 +91,6 @@ pub(super) fn is_overlong(
}
}
// Obtain the start offset of the part of the line that exceeds the limit
let mut start_offset = line.start();
let mut start_width = LineWidth::new(tab_size);
for c in line.chars() {
if start_width < limit {
start_offset += c.text_len();
start_width = start_width.add_char(c);
} else {
break;
}
}
Some(Overlong {
range: TextRange::new(start_offset, line.end()),
width: width.get(),

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Identifier, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -35,11 +35,14 @@ impl Violation for AmbiguousClassName {
}
/// E742
pub(crate) fn ambiguous_class_name(name: &Identifier) -> Option<Diagnostic> {
pub(crate) fn ambiguous_class_name<F>(name: &str, locate: F) -> Option<Diagnostic>
where
F: FnOnce() -> TextRange,
{
if is_ambiguous_name(name) {
Some(Diagnostic::new(
AmbiguousClassName(name.to_string()),
name.range(),
locate(),
))
} else {
None

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Identifier, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -35,11 +35,14 @@ impl Violation for AmbiguousFunctionName {
}
/// E743
pub(crate) fn ambiguous_function_name(name: &Identifier) -> Option<Diagnostic> {
pub(crate) fn ambiguous_function_name<F>(name: &str, locate: F) -> Option<Diagnostic>
where
F: FnOnce() -> TextRange,
{
if is_ambiguous_name(name) {
Some(Diagnostic::new(
AmbiguousFunctionName(name.to_string()),
name.range(),
locate(),
))
} else {
None

View File

@@ -225,7 +225,6 @@ fn function(
decorator_list: vec![],
returns: Some(Box::new(return_type)),
type_comment: None,
type_params: vec![],
range: TextRange::default(),
});
return generator.stmt(&func);
@@ -238,7 +237,6 @@ fn function(
decorator_list: vec![],
returns: None,
type_comment: None,
type_params: vec![],
range: TextRange::default(),
});
generator.stmt(&func)

View File

@@ -1187,22 +1187,19 @@ impl AlwaysAutofixableViolation for SectionNameEndsInColon {
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
#[violation]
pub struct UndocumentedParam {
/// The name of the function being documented.
definition: String,
/// The names of the undocumented parameters.
names: Vec<String>,
pub names: Vec<String>,
}
impl Violation for UndocumentedParam {
#[derive_message_formats]
fn message(&self) -> String {
let UndocumentedParam { definition, names } = self;
let UndocumentedParam { names } = self;
if names.len() == 1 {
let name = &names[0];
format!("Missing argument description in the docstring for `{definition}`: `{name}`")
format!("Missing argument description in the docstring: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Missing argument descriptions in the docstring for `{definition}`: {names}")
format!("Missing argument descriptions in the docstring: {names}")
}
}
}
@@ -1782,16 +1779,11 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
}
if !missing_arg_names.is_empty() {
if let Some(definition) = docstring.definition.name() {
let names = missing_arg_names.into_iter().sorted().collect();
checker.diagnostics.push(Diagnostic::new(
UndocumentedParam {
definition: definition.to_string(),
names,
},
stmt.identifier(),
));
}
let names = missing_arg_names.into_iter().sorted().collect();
checker.diagnostics.push(Diagnostic::new(
UndocumentedParam { names },
stmt.identifier(),
));
}
}

View File

@@ -87,32 +87,6 @@ pub struct Options {
)]
/// Whether to use Google-style or NumPy-style conventions or the PEP257
/// defaults when analyzing docstring sections.
///
/// Enabling a convention will force-disable any rules that are not
/// included in the specified convention. As such, the intended use is
/// to enable a convention and then selectively disable any additional
/// rules on top of it.
///
/// For example, to use Google-style conventions but avoid requiring
/// documentation for every function parameter:
///
/// ```toml
/// [tool.ruff]
/// # Enable all `pydocstyle` rules, limiting to those that adhere to the
/// # Google convention via `convention = "google"`, below.
/// select = ["D"]
///
/// # On top of the Google convention, disable `D417`, which requires
/// # documentation for every function parameter.
/// ignore = ["D417"]
///
/// [tool.ruff.pydocstyle]
/// convention = "google"
/// ```
///
/// As conventions force-disable all rules not included in the convention,
/// enabling _additional_ rules on top of a convention is currently
/// unsupported.
pub convention: Option<Convention>,
#[option(
default = r#"[]"#,

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
sections.py:292:9: D417 Missing argument description in the docstring for `bar`: `y`
sections.py:292:9: D417 Missing argument description in the docstring: `y`
|
290 | x = 1
291 |
@@ -10,7 +10,7 @@ sections.py:292:9: D417 Missing argument description in the docstring for `bar`:
293 | """Nested function test for docstrings.
|
sections.py:309:5: D417 Missing argument description in the docstring for `test_missing_google_args`: `y`
sections.py:309:5: D417 Missing argument description in the docstring: `y`
|
307 | "(argument(s) y are missing descriptions in "
308 | "'test_missing_google_args' docstring)")
@@ -19,7 +19,7 @@ sections.py:309:5: D417 Missing argument description in the docstring for `test_
310 | """Toggle the gizmo.
|
sections.py:333:9: D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `y`, `z`
sections.py:333:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
|
331 | "(argument(s) test, y, z are missing descriptions in "
332 | "'test_missing_args' docstring)", arg_count=5)
@@ -28,7 +28,7 @@ sections.py:333:9: D417 Missing argument descriptions in the docstring for `test
334 | """Test a valid args section.
|
sections.py:345:9: D417 Missing argument descriptions in the docstring for `test_missing_args_class_method`: `test`, `y`, `z`
sections.py:345:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
|
343 | "(argument(s) test, y, z are missing descriptions in "
344 | "'test_missing_args_class_method' docstring)", arg_count=5)
@@ -37,7 +37,7 @@ sections.py:345:9: D417 Missing argument descriptions in the docstring for `test
346 | """Test a valid args section.
|
sections.py:358:9: D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `y`, `z`
sections.py:358:9: D417 Missing argument descriptions in the docstring: `a`, `y`, `z`
|
356 | "(argument(s) a, y, z are missing descriptions in "
357 | "'test_missing_args_static_method' docstring)", arg_count=4)
@@ -46,7 +46,7 @@ sections.py:358:9: D417 Missing argument descriptions in the docstring for `test
359 | """Test a valid args section.
|
sections.py:370:9: D417 Missing argument descriptions in the docstring for `test_missing_docstring`: `a`, `b`
sections.py:370:9: D417 Missing argument descriptions in the docstring: `a`, `b`
|
368 | "(argument(s) a, b are missing descriptions in "
369 | "'test_missing_docstring' docstring)", arg_count=2)
@@ -55,7 +55,7 @@ sections.py:370:9: D417 Missing argument descriptions in the docstring for `test
371 | """Test a valid args section.
|
sections.py:398:5: D417 Missing argument description in the docstring for `test_missing_numpy_args`: `y`
sections.py:398:5: D417 Missing argument description in the docstring: `y`
|
396 | "(argument(s) y are missing descriptions in "
397 | "'test_missing_numpy_args' docstring)")
@@ -64,7 +64,7 @@ sections.py:398:5: D417 Missing argument description in the docstring for `test_
399 | """Toggle the gizmo.
|
sections.py:434:9: D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `y`, `z`
sections.py:434:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
|
432 | "(argument(s) test, y, z are missing descriptions in "
433 | "'test_missing_args' docstring)", arg_count=5)
@@ -73,7 +73,7 @@ sections.py:434:9: D417 Missing argument descriptions in the docstring for `test
435 | """Test a valid args section.
|
sections.py:449:9: D417 Missing argument descriptions in the docstring for `test_missing_args_class_method`: `test`, `y`, `z`
sections.py:449:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
|
447 | "(argument(s) test, y, z are missing descriptions in "
448 | "'test_missing_args_class_method' docstring)", arg_count=4)
@@ -82,7 +82,7 @@ sections.py:449:9: D417 Missing argument descriptions in the docstring for `test
450 | """Test a valid args section.
|
sections.py:468:9: D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `z`
sections.py:468:9: D417 Missing argument descriptions in the docstring: `a`, `z`
|
466 | "(argument(s) a, z are missing descriptions in "
467 | "'test_missing_args_static_method' docstring)", arg_count=3)
@@ -91,7 +91,7 @@ sections.py:468:9: D417 Missing argument descriptions in the docstring for `test
469 | """Test a valid args section.
|
sections.py:498:9: D417 Missing argument description in the docstring for `test_incorrect_indent`: `y`
sections.py:498:9: D417 Missing argument description in the docstring: `y`
|
496 | "(argument(s) y are missing descriptions in "
497 | "'test_incorrect_indent' docstring)", arg_count=3)

View File

@@ -1,63 +1,63 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
D417.py:1:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:1:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
1 | def f(x, y, z):
| ^ D417
2 | """Do something.
|
D417.py:14:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:14:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
14 | def f(x, y, z):
| ^ D417
15 | """Do something.
|
D417.py:27:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:27:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
27 | def f(x, y, z):
| ^ D417
28 | """Do something.
|
D417.py:39:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:39:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
39 | def f(x, y, z):
| ^ D417
40 | """Do something.
|
D417.py:52:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:52:5: D417 Missing argument description in the docstring: `y`
|
52 | def f(x, y, z):
| ^ D417
53 | """Do something.
|
D417.py:65:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:65:5: D417 Missing argument description in the docstring: `y`
|
65 | def f(x, y, z):
| ^ D417
66 | """Do something.
|
D417.py:77:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:77:5: D417 Missing argument description in the docstring: `y`
|
77 | def f(x, y, z):
| ^ D417
78 | """Do something.
|
D417.py:98:5: D417 Missing argument description in the docstring for `f`: `x`
D417.py:98:5: D417 Missing argument description in the docstring: `x`
|
98 | def f(x, *args, **kwargs):
| ^ D417
99 | """Do something.
|
D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*args`
D417.py:108:5: D417 Missing argument description in the docstring: `*args`
|
108 | def f(x, *args, **kwargs):
| ^ D417

View File

@@ -1,63 +1,63 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
D417.py:1:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:1:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
1 | def f(x, y, z):
| ^ D417
2 | """Do something.
|
D417.py:14:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:14:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
14 | def f(x, y, z):
| ^ D417
15 | """Do something.
|
D417.py:27:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:27:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
27 | def f(x, y, z):
| ^ D417
28 | """Do something.
|
D417.py:39:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
D417.py:39:5: D417 Missing argument descriptions in the docstring: `y`, `z`
|
39 | def f(x, y, z):
| ^ D417
40 | """Do something.
|
D417.py:52:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:52:5: D417 Missing argument description in the docstring: `y`
|
52 | def f(x, y, z):
| ^ D417
53 | """Do something.
|
D417.py:65:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:65:5: D417 Missing argument description in the docstring: `y`
|
65 | def f(x, y, z):
| ^ D417
66 | """Do something.
|
D417.py:77:5: D417 Missing argument description in the docstring for `f`: `y`
D417.py:77:5: D417 Missing argument description in the docstring: `y`
|
77 | def f(x, y, z):
| ^ D417
78 | """Do something.
|
D417.py:98:5: D417 Missing argument description in the docstring for `f`: `x`
D417.py:98:5: D417 Missing argument description in the docstring: `x`
|
98 | def f(x, *args, **kwargs):
| ^ D417
99 | """Do something.
|
D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*args`
D417.py:108:5: D417 Missing argument description in the docstring: `*args`
|
108 | def f(x, *args, **kwargs):
| ^ D417

View File

@@ -33,6 +33,7 @@ pub(crate) fn break_outside_loop<'a>(
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) -> Option<Diagnostic> {
let mut allowed: bool = false;
let mut child = stmt;
for parent in parents {
match parent {
@@ -40,7 +41,8 @@ pub(crate) fn break_outside_loop<'a>(
| Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return None;
allowed = true;
break;
}
}
Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => {
@@ -51,5 +53,9 @@ pub(crate) fn break_outside_loop<'a>(
child = parent;
}
Some(Diagnostic::new(BreakOutsideLoop, stmt.range()))
if allowed {
None
} else {
Some(Diagnostic::new(BreakOutsideLoop, stmt.range()))
}
}

View File

@@ -33,6 +33,7 @@ pub(crate) fn continue_outside_loop<'a>(
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) -> Option<Diagnostic> {
let mut allowed: bool = false;
let mut child = stmt;
for parent in parents {
match parent {
@@ -40,7 +41,8 @@ pub(crate) fn continue_outside_loop<'a>(
| Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return None;
allowed = true;
break;
}
}
Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => {
@@ -51,5 +53,9 @@ pub(crate) fn continue_outside_loop<'a>(
child = parent;
}
Some(Diagnostic::new(ContinueOutsideLoop, stmt.range()))
if allowed {
None
} else {
Some(Diagnostic::new(ContinueOutsideLoop, stmt.range()))
}
}

View File

@@ -1,3 +1,5 @@
use rustpython_parser::ast::Ranged;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -53,14 +55,21 @@ impl Violation for GlobalStatement {
/// PLW0603
pub(crate) fn global_statement(checker: &mut Checker, name: &str) {
if let Some(range) = checker.semantic().global(name) {
checker.diagnostics.push(Diagnostic::new(
GlobalStatement {
name: name.to_string(),
},
// Match Pylint's behavior by reporting on the `global` statement`, rather
// than the variable usage.
range,
));
let scope = checker.semantic().scope();
if let Some(binding_id) = scope.get(name) {
let binding = checker.semantic().binding(binding_id);
if binding.is_global() {
if let Some(source) = binding.source {
let source = checker.semantic().stmts[source];
checker.diagnostics.push(Diagnostic::new(
GlobalStatement {
name: name.to_string(),
},
// Match Pylint's behavior by reporting on the `global` statement`, rather
// than the variable usage.
source.range(),
));
}
}
}
}

View File

@@ -82,6 +82,7 @@ fn is_magic_value(constant: &Constant, allowed_types: &[ConstantType]) -> bool {
Constant::Str(value) => !matches!(value.as_str(), "" | "__main__"),
Constant::Int(value) => !matches!(value.try_into(), Ok(0 | 1)),
Constant::Bytes(_) => true,
Constant::Tuple(_) => true,
Constant::Float(_) => true,
Constant::Complex { .. } => true,
}

View File

@@ -150,7 +150,7 @@ pub(crate) fn nested_min_max(
}) {
let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range());
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(expr, checker.locator, checker.indexer) {
if !has_comments(expr, checker.locator) {
let flattened_expr = Expr::Call(ast::ExprCall {
func: Box::new(func.clone()),
args: collect_nested_args(min_max, args, checker.semantic()),

View File

@@ -28,6 +28,7 @@ impl TryFrom<&Constant> for ConstantType {
Constant::Float(..) => Ok(Self::Float),
Constant::Int(..) => Ok(Self::Int),
Constant::Str(..) => Ok(Self::Str),
Constant::Tuple(..) => Ok(Self::Tuple),
Constant::Bool(..) | Constant::Ellipsis | Constant::None => {
Err(anyhow!("Singleton constants are unsupported"))
}

View File

@@ -89,13 +89,12 @@ global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT
82 | CONSTANT = 2
|
global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
global_statement.py:81:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
|
78 | def multiple_assignment():
79 | """Should warn on every assignment."""
80 | global CONSTANT # [global-statement]
| ^^^^^^^^^^^^^^^ PLW0603
81 | CONSTANT = 1
| ^^^^^^^^^^^^ PLW0603
82 | CONSTANT = 2
|

View File

@@ -93,7 +93,11 @@ fn match_named_tuple_assign<'a>(
/// Generate a `Stmt::AnnAssign` representing the provided property
/// definition.
fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt {
fn create_property_assignment_stmt(
property: &str,
annotation: &Expr,
value: Option<&Expr>,
) -> Stmt {
ast::StmtAnnAssign {
target: Box::new(
ast::ExprName {
@@ -104,15 +108,40 @@ fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt {
.into(),
),
annotation: Box::new(annotation.clone()),
value: None,
value: value.map(|value| Box::new(value.clone())),
simple: true,
range: TextRange::default(),
}
.into()
}
/// Create a list of property assignments from the `NamedTuple` fields argument.
fn create_properties_from_fields_arg(fields: &Expr) -> Result<Vec<Stmt>> {
/// Match the `defaults` keyword in a `NamedTuple(...)` call.
fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> {
let defaults = keywords.iter().find(|keyword| {
if let Some(arg) = &keyword.arg {
arg == "defaults"
} else {
false
}
});
match defaults {
Some(defaults) => match &defaults.value {
Expr::List(ast::ExprList { elts, .. }) => Ok(elts),
Expr::Tuple(ast::ExprTuple { elts, .. }) => Ok(elts),
_ => bail!("Expected defaults to be `Expr::List` | `Expr::Tuple`"),
},
None => Ok(&[]),
}
}
/// Create a list of property assignments from the `NamedTuple` arguments.
fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<Stmt>> {
let Some(fields) = args.get(1) else {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
});
return Ok(vec![node]);
};
let Expr::List(ast::ExprList { elts, .. }) = &fields else {
bail!("Expected argument to be `Expr::List`");
};
@@ -122,8 +151,16 @@ fn create_properties_from_fields_arg(fields: &Expr) -> Result<Vec<Stmt>> {
});
return Ok(vec![node]);
}
let padded_defaults = if elts.len() >= defaults.len() {
std::iter::repeat(None)
.take(elts.len() - defaults.len())
.chain(defaults.iter().map(Some))
} else {
bail!("Defaults must be `None` or an iterable of at least the number of fields")
};
elts.iter()
.map(|field| {
.zip(padded_defaults)
.map(|(field, default)| {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &field else {
bail!("Expected `field` to be `Expr::Tuple`")
};
@@ -143,21 +180,9 @@ fn create_properties_from_fields_arg(fields: &Expr) -> Result<Vec<Stmt>> {
if is_dunder(property) {
bail!("Cannot use dunder property name: {}", property)
}
Ok(create_property_assignment_stmt(property, annotation))
})
.collect()
}
/// Create a list of property assignments from the `NamedTuple` keyword arguments.
fn create_properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
keywords
.iter()
.map(|keyword| {
let Keyword { arg, value, .. } = keyword;
let Some(arg) = arg else {
bail!("Expected `keyword` to have an `arg`")
};
Ok(create_property_assignment_stmt(arg.as_str(), value))
Ok(create_property_assignment_stmt(
property, annotation, default,
))
})
.collect()
}
@@ -171,7 +196,6 @@ fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &Expr) ->
keywords: vec![],
body,
decorator_list: vec![],
type_params: vec![],
range: TextRange::default(),
}
.into()
@@ -204,32 +228,12 @@ pub(crate) fn convert_named_tuple_functional_to_class(
return;
};
let properties = match (&args[1..], keywords) {
// Ex) NamedTuple("MyType")
([], []) => vec![Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
})],
// Ex) NamedTuple("MyType", [("a", int), ("b", str)])
([fields], []) => {
if let Ok(properties) = create_properties_from_fields_arg(fields) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse fields");
return;
}
}
// Ex) NamedTuple("MyType", a=int, b=str)
([], keywords) => {
if let Ok(properties) = create_properties_from_keywords(keywords) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse keywords");
return;
}
}
// Unfixable
_ => {
debug!("Skipping `NamedTuple` \"{typename}\": mixed fields and keywords");
let properties = match match_defaults(keywords)
.and_then(|defaults| create_properties_from_args(args, defaults))
{
Ok(properties) => properties,
Err(err) => {
debug!("Skipping `NamedTuple` \"{typename}\": {err}");
return;
}
};

View File

@@ -128,7 +128,6 @@ fn create_class_def_stmt(
keywords,
body,
decorator_list: vec![],
type_params: vec![],
range: TextRange::default(),
}
.into()

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