Compare commits
50 Commits
ag/switch-
...
zb/recursi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9a5a32e6e | ||
|
|
c56fb6e15a | ||
|
|
dbf82233b8 | ||
|
|
87afe36c87 | ||
|
|
704fefc7ab | ||
|
|
dacec7377c | ||
|
|
b669306c87 | ||
|
|
b117f33075 | ||
|
|
c746912b9e | ||
|
|
fc7139d9a5 | ||
|
|
f8f56186b3 | ||
|
|
02fc521369 | ||
|
|
4b0666919b | ||
|
|
06284c3700 | ||
|
|
8d73866f70 | ||
|
|
bc693ea13a | ||
|
|
ad84eedc18 | ||
|
|
96a4f95a44 | ||
|
|
bae26b49a6 | ||
|
|
3d7adbc0ed | ||
|
|
c6456b882c | ||
|
|
49eb97879a | ||
|
|
0c84fbb6db | ||
|
|
a892fc755d | ||
|
|
a067d87ccc | ||
|
|
b64f2ea401 | ||
|
|
4bce801065 | ||
|
|
a56d42f183 | ||
|
|
1d97f27335 | ||
|
|
965adbed4b | ||
|
|
c504d7ab11 | ||
|
|
72c9f7e4c9 | ||
|
|
57be3fce90 | ||
|
|
7a675cd822 | ||
|
|
7b4a73d421 | ||
|
|
91af5a4b74 | ||
|
|
461cdad53a | ||
|
|
b9264a5a11 | ||
|
|
ea79f616bc | ||
|
|
f999b1b617 | ||
|
|
fe6afbe406 | ||
|
|
cbd927f346 | ||
|
|
6159a8e532 | ||
|
|
8ea5b08700 | ||
|
|
4c05c258de | ||
|
|
af6ea2f5e4 | ||
|
|
46ab9dec18 | ||
|
|
d441338358 | ||
|
|
72599dafb6 | ||
|
|
7eaec300dd |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,6 +2,8 @@
|
||||
|
||||
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
|
||||
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
|
||||
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf
|
||||
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
|
||||
|
||||
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
|
||||
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -517,7 +517,7 @@ jobs:
|
||||
path: binaries
|
||||
merge-multiple: true
|
||||
- name: "Publish to GitHub"
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
files: binaries/*
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263))
|
||||
- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297))
|
||||
- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274))
|
||||
- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265))
|
||||
- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280))
|
||||
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
|
||||
- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285))
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117))
|
||||
- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096))
|
||||
- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098))
|
||||
- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140))
|
||||
- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194))
|
||||
- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238))
|
||||
- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215))
|
||||
|
||||
### CLI
|
||||
|
||||
- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110))
|
||||
- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150))
|
||||
- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216))
|
||||
- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220))
|
||||
- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203))
|
||||
- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206))
|
||||
- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587))
|
||||
- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178))
|
||||
|
||||
## 0.3.0
|
||||
|
||||
This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
|
||||
|
||||
@@ -329,13 +329,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
|
||||
|
||||
### Creating a new release
|
||||
|
||||
We use an experimental in-house tool for managing releases.
|
||||
|
||||
1. Install `rooster`: `pip install git+https://github.com/zanieb/rooster@main`
|
||||
1. Run `rooster release`; this command will:
|
||||
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||
1. Run `./scripts/release/bump.sh`; this command will:
|
||||
- Generate a temporary virtual environment with `rooster`
|
||||
- Generate a changelog entry in `CHANGELOG.md`
|
||||
- Update versions in `pyproject.toml` and `Cargo.toml`
|
||||
- Update references to versions in the `README.md` and documentation
|
||||
- Display contributors for the release
|
||||
1. The changelog should then be editorialized for consistency
|
||||
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
|
||||
- Changes should be edited to be user-facing descriptions, avoiding internal details
|
||||
@@ -359,7 +359,7 @@ We use an experimental in-house tool for managing releases.
|
||||
1. Open the draft release in the GitHub release section
|
||||
1. Copy the changelog for the release into the GitHub release
|
||||
- See previous releases for formatting of section headers
|
||||
1. Generate the contributor list with `rooster contributors` and add to the release notes
|
||||
1. Append the contributors from the `bump.sh` script
|
||||
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
|
||||
1. One can determine if an update is needed when
|
||||
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
|
||||
|
||||
192
Cargo.lock
generated
192
Cargo.lock
generated
@@ -270,9 +270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.34"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
|
||||
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -309,9 +309,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.1"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
|
||||
checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -319,9 +319,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.1"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -528,6 +528,19 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.12"
|
||||
@@ -556,6 +569,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.19"
|
||||
@@ -1156,10 +1178,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.68"
|
||||
name = "jod-thread"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
|
||||
checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -1327,6 +1355,31 @@ version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248f65b78f6db5d8e1b1604b4098a28b43d21a8eb1deeca22b1c421b276c7095"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@@ -1950,7 +2003,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -1982,6 +2035,7 @@ dependencies = [
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_server",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"ruff_workspace",
|
||||
@@ -1996,6 +2050,8 @@ dependencies = [
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
"walkdir",
|
||||
"wild",
|
||||
]
|
||||
@@ -2111,7 +2167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2221,7 +2277,6 @@ dependencies = [
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2338,7 +2393,6 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2362,9 +2416,38 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_server"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"ruff_diagnostics",
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_index",
|
||||
"ruff_python_parser",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"ruff_workspace",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2633,6 +2716,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.5"
|
||||
@@ -2956,22 +3050,6 @@ dependencies = [
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -3085,6 +3163,17 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@@ -3111,7 +3200,19 @@ dependencies = [
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-log 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-tree"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ec6adcab41b1391b08a308cc6302b79f8095d1673f6947c2dc65ffb028b0b2d"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"tracing-core",
|
||||
"tracing-log 0.1.4",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3197,9 +3298,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac64ef2f016dc69dfa8283394a70b057066eb054d5fcb6b9eb17bd2ec5097211"
|
||||
checksum = "addeebf294df7922a1164f729fb27ebbbcea99cc32b3bf08afab62757f707677"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"unicode_names2_generator",
|
||||
@@ -3207,15 +3308,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2_generator"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "013f6a731e80f3930de580e55ba41dfa846de4e0fdee4a701f97989cb1597d6a"
|
||||
checksum = "f444b8bba042fe3c1251ffaca35c603f2dc2ccc08d595c65a8c4f76f3e8426c0"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"log",
|
||||
"phf_codegen",
|
||||
"rand",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3354,9 +3454,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
|
||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -3364,9 +3464,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
|
||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -3391,9 +3491,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
|
||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3401,9 +3501,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3414,9 +3514,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -21,8 +21,8 @@ bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.4.1" }
|
||||
bstr = { version = "1.9.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.1", features = ["derive"] }
|
||||
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "2.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
|
||||
@@ -32,6 +32,7 @@ console_error_panic_hook = { version = "0.1.7" }
|
||||
console_log = { version = "1.0.0" }
|
||||
countme = { version = "3.0.1" }
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dirs = { version = "5.0.0" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.10.1" }
|
||||
@@ -51,11 +52,15 @@ insta-cmd = { version = "0.4.0" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.12.1" }
|
||||
js-sys = { version = "0.3.67" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "0.1.2" }
|
||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
lexical-parse-float = { version = "0.8.0", features = ["format"] }
|
||||
libc = { version = "0.2.153" }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
@@ -97,16 +102,17 @@ toml = { version = "0.8.9" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing-tree = { version = "0.2.4" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
unicode_names2 = { version = "1.2.1" }
|
||||
unicode_names2 = { version = "1.2.2" }
|
||||
ureq = { version = "2.9.6" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.84" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
wasm-bindgen-test = { version = "0.3.40" }
|
||||
wild = { version = "2" }
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.0
|
||||
rev: v0.3.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -20,6 +20,7 @@ ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_formatter = { path = "../ruff_python_formatter" }
|
||||
ruff_server = { path = "../ruff_server" }
|
||||
ruff_source_file = { path = "../ruff_source_file" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
ruff_workspace = { path = "../ruff_workspace" }
|
||||
@@ -52,6 +53,8 @@ tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing-subscriber = { workspace = true, features = ["registry"]}
|
||||
tracing-tree = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
wild = { workspace = true }
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ pub enum Command {
|
||||
GenerateShellCompletion { shell: clap_complete_command::Shell },
|
||||
/// Run the Ruff formatter on the given files or directories.
|
||||
Format(FormatCommand),
|
||||
/// Run the language server.
|
||||
Server(ServerCommand),
|
||||
/// Display Ruff's version
|
||||
Version {
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
@@ -494,6 +496,9 @@ pub struct FormatCommand {
|
||||
pub range: Option<FormatRange>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
pub struct ServerCommand;
|
||||
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum HelpFormat {
|
||||
Text,
|
||||
@@ -812,14 +817,24 @@ impl TypedValueParser for ConfigArgumentParser {
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
let path_to_config_file = PathBuf::from(value);
|
||||
if path_to_config_file.exists() {
|
||||
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
|
||||
}
|
||||
// Convert to UTF-8.
|
||||
let Some(value) = value.to_str() else {
|
||||
// But respect non-UTF-8 paths.
|
||||
let path_to_config_file = PathBuf::from(value);
|
||||
if path_to_config_file.is_file() {
|
||||
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
|
||||
}
|
||||
return Err(clap::Error::new(clap::error::ErrorKind::InvalidUtf8));
|
||||
};
|
||||
|
||||
let value = value
|
||||
.to_str()
|
||||
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
|
||||
// Expand environment variables and tildes.
|
||||
if let Ok(path_to_config_file) =
|
||||
shellexpand::full(value).map(|config| PathBuf::from(&*config))
|
||||
{
|
||||
if path_to_config_file.is_file() {
|
||||
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
|
||||
}
|
||||
}
|
||||
|
||||
let config_parse_error = match toml::Table::from_str(value) {
|
||||
Ok(table) => match table.try_into::<Options>() {
|
||||
@@ -887,7 +902,7 @@ A `--config` flag must either be a path to a `.toml` configuration file
|
||||
"
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `{value}` does not exist"
|
||||
The path `{value}` does not point to a configuration file"
|
||||
));
|
||||
}
|
||||
} else if value.contains('=') {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(crate) mod format;
|
||||
pub(crate) mod format_stdin;
|
||||
pub(crate) mod linter;
|
||||
pub(crate) mod rule;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod show_files;
|
||||
pub(crate) mod show_settings;
|
||||
pub(crate) mod version;
|
||||
|
||||
69
crates/ruff/src/commands/server.rs
Normal file
69
crates/ruff/src/commands/server.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::ExitStatus;
|
||||
use anyhow::Result;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_server::Server;
|
||||
use tracing::{level_filters::LevelFilter, metadata::Level, subscriber::Interest, Metadata};
|
||||
use tracing_subscriber::{
|
||||
layer::{Context, Filter, SubscriberExt},
|
||||
Layer, Registry,
|
||||
};
|
||||
use tracing_tree::time::Uptime;
|
||||
|
||||
pub(crate) fn run_server(log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let trace_level = if log_level == LogLevel::Verbose {
|
||||
Level::TRACE
|
||||
} else {
|
||||
Level::DEBUG
|
||||
};
|
||||
|
||||
let subscriber = Registry::default().with(
|
||||
tracing_tree::HierarchicalLayer::default()
|
||||
.with_indent_lines(true)
|
||||
.with_indent_amount(2)
|
||||
.with_bracketed_fields(true)
|
||||
.with_targets(true)
|
||||
.with_writer(|| Box::new(std::io::stderr()))
|
||||
.with_timer(Uptime::default())
|
||||
.with_filter(LoggingFilter { trace_level }),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
let server = Server::new()?;
|
||||
|
||||
server.run().map(|()| ExitStatus::Success)
|
||||
}
|
||||
|
||||
struct LoggingFilter {
|
||||
trace_level: Level,
|
||||
}
|
||||
|
||||
impl LoggingFilter {
|
||||
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
|
||||
let filter = if meta.target().starts_with("ruff") {
|
||||
self.trace_level
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
|
||||
meta.level() <= &filter
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Filter<S> for LoggingFilter {
|
||||
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
|
||||
self.is_enabled(meta)
|
||||
}
|
||||
|
||||
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
|
||||
if self.is_enabled(meta) {
|
||||
Interest::always()
|
||||
} else {
|
||||
Interest::never()
|
||||
}
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
Some(LevelFilter::from_level(self.trace_level))
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use anyhow::Result;
|
||||
use args::GlobalConfigArgs;
|
||||
use args::{GlobalConfigArgs, ServerCommand};
|
||||
use clap::CommandFactory;
|
||||
use colored::Colorize;
|
||||
use log::warn;
|
||||
@@ -190,6 +190,7 @@ pub fn run(
|
||||
}
|
||||
Command::Check(args) => check(args, global_options),
|
||||
Command::Format(args) => format(args, global_options),
|
||||
Command::Server(args) => server(args, global_options.log_level()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +204,12 @@ fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitS
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // TODO: remove once we start taking arguments from here
|
||||
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let ServerCommand {} = args;
|
||||
commands::server::run_server(log_level)
|
||||
}
|
||||
|
||||
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
|
||||
let (cli, config_arguments) = args.partition(global_options)?;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
@@ -34,13 +34,8 @@ pub fn resolve(
|
||||
// Second priority: the user specified a `pyproject.toml` file. Use that
|
||||
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
|
||||
// current working directory. (This matches ESLint's behavior.)
|
||||
if let Some(pyproject) = config_arguments
|
||||
.config_file()
|
||||
.map(|config| config.display().to_string())
|
||||
.map(|config| shellexpand::full(&config).map(|config| PathBuf::from(config.as_ref())))
|
||||
.transpose()?
|
||||
{
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
|
||||
if let Some(pyproject) = config_arguments.config_file() {
|
||||
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?;
|
||||
debug!(
|
||||
"Using user-specified configuration file at: {}",
|
||||
pyproject.display()
|
||||
@@ -48,7 +43,7 @@ pub fn resolve(
|
||||
return Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Fixed,
|
||||
settings,
|
||||
Some(pyproject),
|
||||
Some(pyproject.to_path_buf()),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ fn nonexistent_config_file() {
|
||||
option
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `foo.toml` does not exist
|
||||
The path `foo.toml` does not point to a configuration file
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
|
||||
@@ -522,7 +522,7 @@ fn nonexistent_config_file() {
|
||||
option
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `foo.toml` does not exist
|
||||
The path `foo.toml` does not point to a configuration file
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
@@ -1126,3 +1126,43 @@ import os
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expand environment variables in `--config` paths provided via the CLI.
|
||||
#[test]
|
||||
fn config_expand() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["F"]
|
||||
ignore = ["F841"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg("${NAME}.toml")
|
||||
.env("NAME", "ruff")
|
||||
.arg("-")
|
||||
.current_dir(tempdir.path())
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
|
||||
def func():
|
||||
x = 1
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:2:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -241,7 +241,22 @@ linter.flake8_gettext.functions_names = [
|
||||
ngettext,
|
||||
]
|
||||
linter.flake8_implicit_str_concat.allow_multiline = true
|
||||
linter.flake8_import_conventions.aliases = {"matplotlib": "mpl", "matplotlib.pyplot": "plt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "networkx": "nx", "plotly.express": "px", "polars": "pl", "numpy": "np", "panel": "pn", "pyarrow": "pa", "altair": "alt", "tkinter": "tk", "holoviews": "hv"}
|
||||
linter.flake8_import_conventions.aliases = {
|
||||
altair = alt,
|
||||
holoviews = hv,
|
||||
matplotlib = mpl,
|
||||
matplotlib.pyplot = plt,
|
||||
networkx = nx,
|
||||
numpy = np,
|
||||
pandas = pd,
|
||||
panel = pn,
|
||||
plotly.express = px,
|
||||
polars = pl,
|
||||
pyarrow = pa,
|
||||
seaborn = sns,
|
||||
tensorflow = tf,
|
||||
tkinter = tk,
|
||||
}
|
||||
linter.flake8_import_conventions.banned_aliases = {}
|
||||
linter.flake8_import_conventions.banned_from = []
|
||||
linter.flake8_pytest_style.fixture_parentheses = true
|
||||
|
||||
@@ -545,6 +545,10 @@ impl PrintedRange {
|
||||
&self.code
|
||||
}
|
||||
|
||||
pub fn into_code(self) -> String {
|
||||
self.code
|
||||
}
|
||||
|
||||
/// The range the formatted code corresponds to in the source document.
|
||||
pub fn source_range(&self) -> TextRange {
|
||||
self.source_range
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
22
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S311.py
vendored
Normal file
22
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S311.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
import a_lib
|
||||
|
||||
# OK
|
||||
random.SystemRandom()
|
||||
|
||||
# Errors
|
||||
random.Random()
|
||||
random.random()
|
||||
random.randrange()
|
||||
random.randint()
|
||||
random.choice()
|
||||
random.choices()
|
||||
random.uniform()
|
||||
random.triangular()
|
||||
random.randbytes()
|
||||
|
||||
# Unrelated
|
||||
os.urandom()
|
||||
a_lib.random()
|
||||
@@ -1,52 +1,47 @@
|
||||
import crypt
|
||||
import hashlib
|
||||
from hashlib import new as hashlib_new
|
||||
from hashlib import sha1 as hashlib_sha1
|
||||
|
||||
# Invalid
|
||||
|
||||
# Errors
|
||||
hashlib.new('md5')
|
||||
|
||||
hashlib.new('md4', b'test')
|
||||
|
||||
hashlib.new(name='md5', data=b'test')
|
||||
|
||||
hashlib.new('MD4', data=b'test')
|
||||
|
||||
hashlib.new('sha1')
|
||||
|
||||
hashlib.new('sha1', data=b'test')
|
||||
|
||||
hashlib.new('sha', data=b'test')
|
||||
|
||||
hashlib.new(name='SHA', data=b'test')
|
||||
|
||||
hashlib.sha(data=b'test')
|
||||
|
||||
hashlib.md5()
|
||||
|
||||
hashlib_new('sha1')
|
||||
|
||||
hashlib_sha1('sha1')
|
||||
|
||||
# usedforsecurity arg only available in Python 3.9+
|
||||
hashlib.new('sha1', usedforsecurity=True)
|
||||
|
||||
# Valid
|
||||
crypt.crypt("test", salt=crypt.METHOD_CRYPT)
|
||||
crypt.crypt("test", salt=crypt.METHOD_MD5)
|
||||
crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
|
||||
crypt.crypt("test", crypt.METHOD_BLOWFISH)
|
||||
|
||||
crypt.mksalt(crypt.METHOD_CRYPT)
|
||||
crypt.mksalt(crypt.METHOD_MD5)
|
||||
crypt.mksalt(crypt.METHOD_BLOWFISH)
|
||||
|
||||
# OK
|
||||
hashlib.new('sha256')
|
||||
|
||||
hashlib.new('SHA512')
|
||||
|
||||
hashlib.sha256(data=b'test')
|
||||
|
||||
# usedforsecurity arg only available in Python 3.9+
|
||||
hashlib_new(name='sha1', usedforsecurity=False)
|
||||
|
||||
# usedforsecurity arg only available in Python 3.9+
|
||||
hashlib_sha1(name='sha1', usedforsecurity=False)
|
||||
|
||||
# usedforsecurity arg only available in Python 3.9+
|
||||
hashlib.md4(usedforsecurity=False)
|
||||
|
||||
# usedforsecurity arg only available in Python 3.9+
|
||||
hashlib.new(name='sha256', usedforsecurity=False)
|
||||
|
||||
crypt.crypt("test")
|
||||
crypt.crypt("test", salt=crypt.METHOD_SHA256)
|
||||
crypt.crypt("test", salt=crypt.METHOD_SHA512)
|
||||
|
||||
crypt.mksalt()
|
||||
crypt.mksalt(crypt.METHOD_SHA256)
|
||||
crypt.mksalt(crypt.METHOD_SHA512)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import commands
|
||||
import popen2
|
||||
@@ -16,6 +17,8 @@ popen2.Popen3("true")
|
||||
popen2.Popen4("true")
|
||||
commands.getoutput("true")
|
||||
commands.getstatusoutput("true")
|
||||
subprocess.getoutput("true")
|
||||
subprocess.getstatusoutput("true")
|
||||
|
||||
|
||||
# Check command argument looks unsafe.
|
||||
|
||||
34
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S610.py
vendored
Normal file
34
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S610.py
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Errors
|
||||
User.objects.filter(username='admin').extra(dict(could_be='insecure'))
|
||||
User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
|
||||
User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
|
||||
User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
|
||||
|
||||
query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
|
||||
User.objects.filter(username='admin').extra(select={'test': query})
|
||||
|
||||
where_var = ['1=1) OR 1=1 AND (1=1']
|
||||
User.objects.filter(username='admin').extra(where=where_var)
|
||||
|
||||
where_str = '1=1) OR 1=1 AND (1=1'
|
||||
User.objects.filter(username='admin').extra(where=[where_str])
|
||||
|
||||
tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
|
||||
User.objects.all().extra(tables=tables_var).distinct()
|
||||
|
||||
tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
|
||||
User.objects.all().extra(tables=[tables_str]).distinct()
|
||||
|
||||
# OK
|
||||
User.objects.filter(username='admin').extra(
|
||||
select={'test': 'secure'},
|
||||
where=['secure'],
|
||||
tables=['secure']
|
||||
)
|
||||
User.objects.filter(username='admin').extra({'test': 'secure'})
|
||||
User.objects.filter(username='admin').extra(select={'test': 'secure'})
|
||||
User.objects.filter(username='admin').extra(where=['secure'])
|
||||
@@ -14,9 +14,6 @@ reversed(sorted(x, reverse=not x))
|
||||
reversed(sorted(i for i in range(42)))
|
||||
reversed(sorted((i for i in range(42)), reverse=True))
|
||||
|
||||
|
||||
def reversed(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
reversed(sorted(x, reverse=True))
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
|
||||
@@ -40,4 +40,7 @@ f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004
|
||||
|
||||
# Make sure we do not unescape quotes
|
||||
this_is_fine = "This is an \\'escaped\\' quote"
|
||||
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash"
|
||||
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
|
||||
|
||||
# Invalid escapes in bytestrings are also triggered:
|
||||
x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
|
||||
|
||||
8
crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# These rules test for intentional "odd" formatting. Using a formatter fixes that
|
||||
[E{1,2,3}*.py]
|
||||
generated_code = true
|
||||
ij_formatter_enabled = false
|
||||
|
||||
[W*.py]
|
||||
generated_code = true
|
||||
ij_formatter_enabled = false
|
||||
@@ -153,3 +153,9 @@ ham[lower +1 :, "columnname"]
|
||||
|
||||
#: E203:1:13
|
||||
ham[lower + 1 :, "columnname"]
|
||||
|
||||
#: Okay
|
||||
f"{ham[lower +1 :, "columnname"]}"
|
||||
|
||||
#: E203:1:13
|
||||
f"{ham[lower + 1 :, "columnname"]}"
|
||||
|
||||
1
crates/ruff_linter/resources/test/fixtures/pycodestyle/E2_syntax_error.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/pycodestyle/E2_syntax_error.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
a = (1 or)
|
||||
50
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi
vendored
Normal file
50
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
class MissingCommand(TypeError): ...
|
||||
class AnoherClass: ...
|
||||
|
||||
def a(): ...
|
||||
|
||||
@overload
|
||||
def a(arg: int): ...
|
||||
|
||||
@overload
|
||||
def a(arg: int, name: str): ...
|
||||
|
||||
|
||||
def grouped1(): ...
|
||||
def grouped2(): ...
|
||||
def grouped3( ): ...
|
||||
|
||||
|
||||
class BackendProxy:
|
||||
backend_module: str
|
||||
backend_object: str | None
|
||||
backend: Any
|
||||
|
||||
def grouped1(): ...
|
||||
def grouped2(): ...
|
||||
def grouped3( ): ...
|
||||
@decorated
|
||||
|
||||
def with_blank_line(): ...
|
||||
|
||||
|
||||
def ungrouped(): ...
|
||||
a = "test"
|
||||
|
||||
def function_def():
|
||||
pass
|
||||
b = "test"
|
||||
|
||||
|
||||
def outer():
|
||||
def inner():
|
||||
pass
|
||||
def inner2():
|
||||
pass
|
||||
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
62
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py
vendored
Normal file
62
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
|
||||
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class MissingCommand(TypeError): ... # noqa: N818
|
||||
|
||||
|
||||
class BackendProxy:
|
||||
backend_module: str
|
||||
backend_object: str | None
|
||||
backend: Any
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import abcd
|
||||
|
||||
|
||||
abcd.foo()
|
||||
|
||||
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
|
||||
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
abcd.foo()
|
||||
|
||||
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
def _exit(self) -> None: ...
|
||||
|
||||
|
||||
def _optional_commands(self) -> dict[str, bool]: ...
|
||||
|
||||
|
||||
def run(argv: Sequence[str]) -> int: ...
|
||||
|
||||
|
||||
def read_line(fd: int = 0) -> bytearray: ...
|
||||
|
||||
|
||||
def flush() -> None: ...
|
||||
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
class MissingCommand(TypeError): ... # noqa: N818
|
||||
62
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.pyi
vendored
Normal file
62
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.pyi
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
|
||||
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class MissingCommand(TypeError): ... # noqa: N818
|
||||
|
||||
|
||||
class BackendProxy:
|
||||
backend_module: str
|
||||
backend_object: str | None
|
||||
backend: Any
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import abcd
|
||||
|
||||
|
||||
abcd.foo()
|
||||
|
||||
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
|
||||
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
abcd.foo()
|
||||
|
||||
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
def _exit(self) -> None: ...
|
||||
|
||||
|
||||
def _optional_commands(self) -> dict[str, bool]: ...
|
||||
|
||||
|
||||
def run(argv: Sequence[str]) -> int: ...
|
||||
|
||||
|
||||
def read_line(fd: int = 0) -> bytearray: ...
|
||||
|
||||
|
||||
def flush() -> None: ...
|
||||
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
class MissingCommand(TypeError): ... # noqa: N818
|
||||
88
crates/ruff_linter/resources/test/fixtures/pycodestyle/E502.py
vendored
Normal file
88
crates/ruff_linter/resources/test/fixtures/pycodestyle/E502.py
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
a = 2 + 2
|
||||
|
||||
a = (2 + 2)
|
||||
|
||||
a = 2 + \
|
||||
3 \
|
||||
+ 4
|
||||
|
||||
a = (3 -\
|
||||
2 + \
|
||||
7)
|
||||
|
||||
z = 5 + \
|
||||
(3 -\
|
||||
2 + \
|
||||
7) + \
|
||||
4
|
||||
|
||||
b = [2 +
|
||||
2]
|
||||
|
||||
b = [
|
||||
2 + 4 + 5 + \
|
||||
44 \
|
||||
- 5
|
||||
]
|
||||
|
||||
c = (True and
|
||||
False \
|
||||
or False \
|
||||
and True \
|
||||
)
|
||||
|
||||
c = (True and
|
||||
False)
|
||||
|
||||
d = True and \
|
||||
False or \
|
||||
False \
|
||||
and not True
|
||||
|
||||
|
||||
s = {
|
||||
'x': 2 + \
|
||||
2
|
||||
}
|
||||
|
||||
|
||||
s = {
|
||||
'x': 2 +
|
||||
2
|
||||
}
|
||||
|
||||
|
||||
x = {2 + 4 \
|
||||
+ 3}
|
||||
|
||||
y = (
|
||||
2 + 2 # \
|
||||
+ 3 # \
|
||||
+ 4 \
|
||||
+ 3
|
||||
)
|
||||
|
||||
|
||||
x = """
|
||||
(\\
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
("""hello \
|
||||
""")
|
||||
|
||||
("hello \
|
||||
")
|
||||
|
||||
|
||||
x = "abc" \
|
||||
"xyz"
|
||||
|
||||
x = ("abc" \
|
||||
"xyz")
|
||||
|
||||
|
||||
def foo():
|
||||
x = (a + \
|
||||
2)
|
||||
14
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py
vendored
Normal file
14
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Unix style
|
||||
def foo() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar() -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
foo()
|
||||
bar()
|
||||
|
||||
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Unix style
|
||||
def foo() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar() -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
foo()
|
||||
bar()
|
||||
17
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Windows style
|
||||
def foo() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar() -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
foo()
|
||||
bar()
|
||||
|
||||
|
||||
|
||||
|
||||
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Windows style
|
||||
def foo() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar() -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
foo()
|
||||
bar()
|
||||
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# This is fine
|
||||
def foo():
|
||||
pass
|
||||
|
||||
# Some comment
|
||||
7
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_23.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_23.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Test: ensure that we treat strings in `typing.Annotation` as type definitions."""
|
||||
|
||||
from pathlib import Path
|
||||
from re import RegexFlag
|
||||
from typing import Annotated
|
||||
|
||||
p: Annotated["Path", int] = 1
|
||||
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_11.pyi
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_11.pyi
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Test case: strings used within calls within type annotations."""
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import bpy
|
||||
from mypy_extensions import VarArg
|
||||
|
||||
class LightShow(bpy.types.Operator):
|
||||
label = "Create Character"
|
||||
name = "lightshow.letter_creation"
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # OK
|
||||
|
||||
|
||||
def f(x: Callable[[VarArg("os")], None]): # F821
|
||||
pass
|
||||
44
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py
vendored
Normal file
44
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Tests for constructs allowed in `.pyi` stub files but not at runtime"""
|
||||
|
||||
from typing import Optional, TypeAlias, Union
|
||||
|
||||
__version__: str
|
||||
__author__: str
|
||||
|
||||
# Forward references:
|
||||
MaybeCStr: TypeAlias = Optional[CStr] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
MaybeCStr2: TypeAlias = Optional["CStr"] # always okay
|
||||
CStr: TypeAlias = Union[C, str] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
CStr2: TypeAlias = Union["C", str] # always okay
|
||||
|
||||
# References to a class from inside the class:
|
||||
class C:
|
||||
other: C = ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
other2: "C" = ... # always okay
|
||||
def from_str(self, s: str) -> C: ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
def from_str2(self, s: str) -> "C": ... # always okay
|
||||
|
||||
# Circular references:
|
||||
class A:
|
||||
foo: B # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
foo2: "B" # always okay
|
||||
bar: dict[str, B] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
bar2: dict[str, "A"] # always okay
|
||||
|
||||
class B:
|
||||
foo: A # always okay
|
||||
bar: dict[str, A] # always okay
|
||||
|
||||
class Leaf: ...
|
||||
class Tree(list[Tree | Leaf]): ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
class Tree2(list["Tree | Leaf"]): ... # always okay
|
||||
|
||||
# Annotations are treated as assignments in .pyi files, but not in .py files
|
||||
class MyClass:
|
||||
foo: int
|
||||
bar = foo # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
bar = "foo" # always okay
|
||||
|
||||
baz: MyClass
|
||||
eggs = baz # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
eggs = "baz" # always okay
|
||||
44
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.pyi
vendored
Normal file
44
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.pyi
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Tests for constructs allowed in `.pyi` stub files but not at runtime"""
|
||||
|
||||
from typing import Optional, TypeAlias, Union
|
||||
|
||||
__version__: str
|
||||
__author__: str
|
||||
|
||||
# Forward references:
|
||||
MaybeCStr: TypeAlias = Optional[CStr] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
MaybeCStr2: TypeAlias = Optional["CStr"] # always okay
|
||||
CStr: TypeAlias = Union[C, str] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
CStr2: TypeAlias = Union["C", str] # always okay
|
||||
|
||||
# References to a class from inside the class:
|
||||
class C:
|
||||
other: C = ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
other2: "C" = ... # always okay
|
||||
def from_str(self, s: str) -> C: ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
def from_str2(self, s: str) -> "C": ... # always okay
|
||||
|
||||
# Circular references:
|
||||
class A:
|
||||
foo: B # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
foo2: "B" # always okay
|
||||
bar: dict[str, B] # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
bar2: dict[str, "A"] # always okay
|
||||
|
||||
class B:
|
||||
foo: A # always okay
|
||||
bar: dict[str, A] # always okay
|
||||
|
||||
class Leaf: ...
|
||||
class Tree(list[Tree | Leaf]): ... # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
class Tree2(list["Tree | Leaf"]): ... # always okay
|
||||
|
||||
# Annotations are treated as assignments in .pyi files, but not in .py files
|
||||
class MyClass:
|
||||
foo: int
|
||||
bar = foo # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
bar = "foo" # always okay
|
||||
|
||||
baz: MyClass
|
||||
eggs = baz # valid in a `.pyi` stub file, not in a `.py` runtime file
|
||||
eggs = "baz" # always okay
|
||||
48
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py
vendored
Normal file
48
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Tests for constructs allowed when `__future__` annotations are enabled but not otherwise"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TypeAlias, Union
|
||||
|
||||
__version__: str
|
||||
__author__: str
|
||||
|
||||
# References to a class from inside the class:
|
||||
class C:
|
||||
other: C = ... # valid when `__future__.annotations are enabled
|
||||
other2: "C" = ... # always okay
|
||||
def from_str(self, s: str) -> C: ... # valid when `__future__.annotations are enabled
|
||||
def from_str2(self, s: str) -> "C": ... # always okay
|
||||
|
||||
# Circular references:
|
||||
class A:
|
||||
foo: B # valid when `__future__.annotations are enabled
|
||||
foo2: "B" # always okay
|
||||
bar: dict[str, B] # valid when `__future__.annotations are enabled
|
||||
bar2: dict[str, "A"] # always okay
|
||||
|
||||
class B:
|
||||
foo: A # always okay
|
||||
bar: dict[str, A] # always okay
|
||||
|
||||
# Annotations are treated as assignments in .pyi files, but not in .py files
|
||||
class MyClass:
|
||||
foo: int
|
||||
bar = foo # Still invalid even when `__future__.annotations` are enabled
|
||||
bar = "foo" # always okay
|
||||
|
||||
baz: MyClass
|
||||
eggs = baz # Still invalid even when `__future__.annotations` are enabled
|
||||
eggs = "baz" # always okay
|
||||
|
||||
# Forward references:
|
||||
MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled
|
||||
MaybeDStr2: TypeAlias = Optional["DStr"] # always okay
|
||||
DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled
|
||||
DStr2: TypeAlias = Union["D", str] # always okay
|
||||
|
||||
class D: ...
|
||||
|
||||
# More circular references
|
||||
class Leaf: ...
|
||||
class Tree(list[Tree | Leaf]): ... # Still invalid even when `__future__.annotations` are enabled
|
||||
class Tree2(list["Tree | Leaf"]): ... # always okay
|
||||
10
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_5.pyi
vendored
Normal file
10
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_5.pyi
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Test: inner class annotation."""
|
||||
|
||||
class RandomClass:
|
||||
def bad_func(self) -> InnerClass: ... # F821
|
||||
def good_func(self) -> OuterClass.InnerClass: ... # Okay
|
||||
|
||||
class OuterClass:
|
||||
class InnerClass: ...
|
||||
|
||||
def good_func(self) -> InnerClass: ... # Okay
|
||||
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F822_0.pyi
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F822_0.pyi
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
a = 1
|
||||
b: int # Considered a binding in a `.pyi` stub file, not in a `.py` runtime file
|
||||
|
||||
__all__ = ["a", "b", "c"] # c is flagged as missing; b is not
|
||||
@@ -54,3 +54,15 @@ class StudentE(StudentD):
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
class StudentF(object):
|
||||
__slots__ = ("name", "__dict__")
|
||||
|
||||
def __init__(self, name, middle_name):
|
||||
self.name = name
|
||||
self.middle_name = middle_name # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
@@ -252,3 +252,10 @@ raise ValueError(
|
||||
|
||||
# The dictionary should be parenthesized.
|
||||
"{}".format({0: 1}())
|
||||
|
||||
# The string shouldn't be converted, since it would require repeating the function call.
|
||||
"{x} {x}".format(x=foo())
|
||||
"{0} {0}".format(foo())
|
||||
|
||||
# The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
"{0} {1}".format(foo(), foo())
|
||||
|
||||
@@ -427,7 +427,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::format_literals(checker, call, &summary);
|
||||
}
|
||||
if checker.enabled(Rule::FString) {
|
||||
pyupgrade::rules::f_strings(checker, call, &summary, value);
|
||||
pyupgrade::rules::f_strings(checker, call, &summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,6 +632,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
]) {
|
||||
flake8_bandit::rules::shell_injection(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoExtra) {
|
||||
flake8_bandit::rules::django_extra(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoRawSql) {
|
||||
flake8_bandit::rules::django_raw_sql(checker, call);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
|
||||
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_variable_name(¶meter.name, parameter.range())
|
||||
pycodestyle::rules::ambiguous_variable_name(¶meter.name, parameter.name.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -419,11 +419,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
self.semantic.add_module(module);
|
||||
|
||||
if alias.asname.is_none() && alias.name.contains('.') {
|
||||
let qualified_name = QualifiedName::imported(&alias.name);
|
||||
let qualified_name = QualifiedName::user_defined(&alias.name);
|
||||
self.add_binding(
|
||||
module,
|
||||
alias.identifier(),
|
||||
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }),
|
||||
BindingKind::SubmoduleImport(SubmoduleImport {
|
||||
qualified_name: Box::new(qualified_name),
|
||||
}),
|
||||
BindingFlags::EXTERNAL,
|
||||
);
|
||||
} else {
|
||||
@@ -440,11 +442,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
|
||||
let name = alias.asname.as_ref().unwrap_or(&alias.name);
|
||||
let qualified_name = QualifiedName::imported(&alias.name);
|
||||
let qualified_name = QualifiedName::user_defined(&alias.name);
|
||||
self.add_binding(
|
||||
name,
|
||||
alias.identifier(),
|
||||
BindingKind::Import(Import { qualified_name }),
|
||||
BindingKind::Import(Import {
|
||||
qualified_name: Box::new(qualified_name),
|
||||
}),
|
||||
flags,
|
||||
);
|
||||
}
|
||||
@@ -508,7 +512,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
self.add_binding(
|
||||
name,
|
||||
alias.identifier(),
|
||||
BindingKind::FromImport(FromImport { qualified_name }),
|
||||
BindingKind::FromImport(FromImport {
|
||||
qualified_name: Box::new(qualified_name),
|
||||
}),
|
||||
flags,
|
||||
);
|
||||
}
|
||||
@@ -932,6 +938,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
&& !self.semantic.in_deferred_type_definition()
|
||||
&& self.semantic.in_type_definition()
|
||||
&& self.semantic.future_annotations()
|
||||
&& (self.semantic.in_typing_only_annotation() || self.source_type.is_stub())
|
||||
{
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
|
||||
self.visit.string_type_definitions.push((
|
||||
@@ -1342,7 +1349,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
{
|
||||
let mut iter = elts.iter();
|
||||
if let Some(expr) = iter.next() {
|
||||
self.visit_expr(expr);
|
||||
self.visit_type_definition(expr);
|
||||
}
|
||||
for expr in iter {
|
||||
self.visit_non_type_definition(expr);
|
||||
@@ -1833,11 +1840,13 @@ impl<'a> Checker<'a> {
|
||||
flags.insert(BindingFlags::UNPACKED_ASSIGNMENT);
|
||||
}
|
||||
|
||||
// Match the left-hand side of an annotated assignment, like `x` in `x: int`.
|
||||
// Match the left-hand side of an annotated assignment without a value,
|
||||
// like `x` in `x: int`. N.B. In stub files, these should be viewed
|
||||
// as assignments on par with statements such as `x: int = 5`.
|
||||
if matches!(
|
||||
parent,
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { value: None, .. })
|
||||
) && !self.semantic.in_annotation()
|
||||
) && !(self.semantic.in_annotation() || self.source_type.is_stub())
|
||||
{
|
||||
self.add_binding(id, expr.range(), BindingKind::Annotation, flags);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::line_width::IndentWidth;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -9,8 +10,8 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{
|
||||
extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword,
|
||||
missing_whitespace_around_operator, space_after_comma, space_around_operator,
|
||||
whitespace_around_keywords, whitespace_around_named_parameter_equals,
|
||||
missing_whitespace_around_operator, redundant_backslash, space_after_comma,
|
||||
space_around_operator, whitespace_around_keywords, whitespace_around_named_parameter_equals,
|
||||
whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags,
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
@@ -35,6 +36,7 @@ pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize {
|
||||
pub(crate) fn check_logical_lines(
|
||||
tokens: &[LexResult],
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
settings: &LinterSettings,
|
||||
) -> Vec<Diagnostic> {
|
||||
@@ -73,6 +75,7 @@ pub(crate) fn check_logical_lines(
|
||||
|
||||
if line.flags().contains(TokenFlags::BRACKET) {
|
||||
whitespace_before_parameters(&line, &mut context);
|
||||
redundant_backslash(&line, locator, indexer, &mut context);
|
||||
}
|
||||
|
||||
// Extract the indentation level.
|
||||
|
||||
@@ -41,14 +41,8 @@ pub(crate) fn check_tokens(
|
||||
Rule::BlankLinesAfterFunctionOrClass,
|
||||
Rule::BlankLinesBeforeNestedDefinition,
|
||||
]) {
|
||||
let mut blank_lines_checker = BlankLinesChecker::default();
|
||||
blank_lines_checker.check_lines(
|
||||
tokens,
|
||||
locator,
|
||||
stylist,
|
||||
settings.tab_size,
|
||||
&mut diagnostics,
|
||||
);
|
||||
BlankLinesChecker::new(locator, stylist, settings, source_type)
|
||||
.check_lines(tokens, &mut diagnostics);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::BlanketNOQA) {
|
||||
@@ -209,6 +203,10 @@ pub(crate) fn check_tokens(
|
||||
flake8_fixme::rules::todos(&mut diagnostics, &todo_comments);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) {
|
||||
pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens);
|
||||
}
|
||||
|
||||
diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule()));
|
||||
|
||||
diagnostics
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine),
|
||||
(Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
|
||||
(Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong),
|
||||
(Pycodestyle, "E502") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::RedundantBackslash),
|
||||
(Pycodestyle, "E701") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineColon),
|
||||
(Pycodestyle, "E702") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon),
|
||||
(Pycodestyle, "E703") => (RuleGroup::Stable, rules::pycodestyle::rules::UselessSemicolon),
|
||||
@@ -167,6 +168,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pycodestyle, "W291") => (RuleGroup::Stable, rules::pycodestyle::rules::TrailingWhitespace),
|
||||
(Pycodestyle, "W292") => (RuleGroup::Stable, rules::pycodestyle::rules::MissingNewlineAtEndOfFile),
|
||||
(Pycodestyle, "W293") => (RuleGroup::Stable, rules::pycodestyle::rules::BlankLineWithWhitespace),
|
||||
(Pycodestyle, "W391") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyNewlinesAtEndOfFile),
|
||||
(Pycodestyle, "W505") => (RuleGroup::Stable, rules::pycodestyle::rules::DocLineTooLong),
|
||||
(Pycodestyle, "W605") => (RuleGroup::Stable, rules::pycodestyle::rules::InvalidEscapeSequence),
|
||||
|
||||
@@ -680,6 +682,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
|
||||
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
|
||||
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
|
||||
(Flake8Bandit, "610") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoExtra),
|
||||
(Flake8Bandit, "611") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoRawSql),
|
||||
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
|
||||
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use libcst_native::{
|
||||
Expression, Name, ParenthesizableWhitespace, SimpleWhitespace, UnaryOperation,
|
||||
Expression, LeftParen, Name, ParenthesizableWhitespace, ParenthesizedNode, RightParen,
|
||||
SimpleWhitespace, UnaryOperation,
|
||||
};
|
||||
|
||||
/// Return a [`ParenthesizableWhitespace`] containing a single space.
|
||||
@@ -24,6 +25,7 @@ pub(crate) fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// If the expression is `True` or `False`, return the opposite.
|
||||
if let Expression::Name(ref expression) = expression {
|
||||
match expression.value {
|
||||
"True" => {
|
||||
@@ -44,11 +46,32 @@ pub(crate) fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// If the expression is higher precedence than the unary `not`, we need to wrap it in
|
||||
// parentheses.
|
||||
//
|
||||
// For example: given `a and b`, we need to return `not (a and b)`, rather than `not a and b`.
|
||||
//
|
||||
// See: <https://docs.python.org/3/reference/expressions.html#operator-precedence>
|
||||
let needs_parens = matches!(
|
||||
expression,
|
||||
Expression::BooleanOperation(_)
|
||||
| Expression::IfExp(_)
|
||||
| Expression::Lambda(_)
|
||||
| Expression::NamedExpr(_)
|
||||
);
|
||||
let has_parens = !expression.lpar().is_empty() && !expression.rpar().is_empty();
|
||||
// Otherwise, wrap in a `not` operator.
|
||||
Expression::UnaryOperation(Box::new(UnaryOperation {
|
||||
operator: libcst_native::UnaryOp::Not {
|
||||
whitespace_after: space(),
|
||||
},
|
||||
expression: Box::new(expression.clone()),
|
||||
expression: Box::new(if needs_parens && !has_parens {
|
||||
expression
|
||||
.clone()
|
||||
.with_parens(LeftParen::default(), RightParen::default())
|
||||
} else {
|
||||
expression.clone()
|
||||
}),
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}))
|
||||
|
||||
@@ -131,10 +131,7 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
|
||||
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string.
|
||||
Tok::String {
|
||||
triple_quoted: true,
|
||||
..
|
||||
} => {
|
||||
Tok::String { kind, .. } if kind.is_triple_quoted() => {
|
||||
if locator.contains_line_break(*range) {
|
||||
string_mappings.push(TextRange::new(
|
||||
locator.line_start(range.start()),
|
||||
|
||||
@@ -418,29 +418,6 @@ pub(crate) fn fits(
|
||||
all_lines_fit(fix, node, locator, line_length.value() as usize, tab_size)
|
||||
}
|
||||
|
||||
/// Returns `true` if the fix fits within the maximum configured line length, or produces lines that
|
||||
/// are shorter than the maximum length of the existing AST node.
|
||||
pub(crate) fn fits_or_shrinks(
|
||||
fix: &str,
|
||||
node: AnyNodeRef,
|
||||
locator: &Locator,
|
||||
line_length: LineLength,
|
||||
tab_size: IndentWidth,
|
||||
) -> bool {
|
||||
// Use the larger of the line length limit, or the longest line in the existing AST node.
|
||||
let line_length = std::iter::once(line_length.value() as usize)
|
||||
.chain(
|
||||
locator
|
||||
.slice(locator.lines_range(node.range()))
|
||||
.universal_newlines()
|
||||
.map(|line| LineWidthBuilder::new(tab_size).add_str(&line).get()),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or(line_length.value() as usize);
|
||||
|
||||
all_lines_fit(fix, node, locator, line_length, tab_size)
|
||||
}
|
||||
|
||||
/// Returns `true` if all lines in the fix are shorter than the given line length.
|
||||
fn all_lines_fit(
|
||||
fix: &str,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! and subject to change drastically.
|
||||
//!
|
||||
//! [Ruff]: https://github.com/astral-sh/ruff
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[cfg(feature = "clap")]
|
||||
pub use registry::clap_completion::RuleParser;
|
||||
|
||||
@@ -132,7 +132,7 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_logical_lines())
|
||||
{
|
||||
diagnostics.extend(crate::checkers::logical_lines::check_logical_lines(
|
||||
&tokens, locator, stylist, settings,
|
||||
&tokens, locator, indexer, stylist, settings,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +300,7 @@ impl Rule {
|
||||
| Rule::SingleLineImplicitStringConcatenation
|
||||
| Rule::TabIndentation
|
||||
| Rule::TooManyBlankLines
|
||||
| Rule::TooManyNewlinesAtEndOfFile
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::UselessSemicolon
|
||||
@@ -327,6 +328,7 @@ impl Rule {
|
||||
| Rule::NoSpaceAfterBlockComment
|
||||
| Rule::NoSpaceAfterInlineComment
|
||||
| Rule::OverIndented
|
||||
| Rule::RedundantBackslash
|
||||
| Rule::TabAfterComma
|
||||
| Rule::TabAfterKeyword
|
||||
| Rule::TabAfterOperator
|
||||
|
||||
@@ -48,6 +48,7 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
|
||||
#[test_case(Rule::SuspiciousMarkSafeUsage, Path::new("S308.py"))]
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
|
||||
#[test_case(Rule::SuspiciousFtplibImport, Path::new("S402.py"))]
|
||||
@@ -68,6 +69,7 @@ mod tests {
|
||||
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]
|
||||
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))]
|
||||
#[test_case(Rule::WeakCryptographicKey, Path::new("S505.py"))]
|
||||
#[test_case(Rule::DjangoExtra, Path::new("S610.py"))]
|
||||
#[test_case(Rule::DjangoRawSql, Path::new("S611.py"))]
|
||||
#[test_case(Rule::TarfileUnsafeMembers, Path::new("S202.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprDict, ExprList};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of Django's `extra` function.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Django's `extra` function can be used to execute arbitrary SQL queries,
|
||||
/// which can in turn lead to SQL injection vulnerabilities.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from django.contrib.auth.models import User
|
||||
///
|
||||
/// User.objects.all().extra(select={"test": "%secure" % "nos"})
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection)
|
||||
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html)
|
||||
#[violation]
|
||||
pub struct DjangoExtra;
|
||||
|
||||
impl Violation for DjangoExtra {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use of Django `extra` can lead to SQL injection vulnerabilities")
|
||||
}
|
||||
}
|
||||
|
||||
/// S610
|
||||
pub(crate) fn django_extra(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Expr::Attribute(ExprAttribute { attr, .. }) = call.func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if attr.as_str() != "extra" {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_call_insecure(call) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoExtra, call.arguments.range()));
|
||||
}
|
||||
}
|
||||
|
||||
fn is_call_insecure(call: &ast::ExprCall) -> bool {
|
||||
for (argument_name, position) in [("select", 0), ("where", 1), ("tables", 3)] {
|
||||
if let Some(argument) = call.arguments.find_argument(argument_name, position) {
|
||||
match argument_name {
|
||||
"select" => match argument {
|
||||
Expr::Dict(ExprDict { keys, values, .. }) => {
|
||||
if !keys.iter().flatten().all(Expr::is_string_literal_expr) {
|
||||
return true;
|
||||
}
|
||||
if !values.iter().all(Expr::is_string_literal_expr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => return true,
|
||||
},
|
||||
"where" | "tables" => match argument {
|
||||
Expr::List(ExprList { elts, .. }) => {
|
||||
if !elts.iter().all(Expr::is_string_literal_expr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => return true,
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -9,7 +9,8 @@ use crate::checkers::ast::Checker;
|
||||
use super::super::helpers::string_literal;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of weak or broken cryptographic hash functions.
|
||||
/// Checks for uses of weak or broken cryptographic hash functions in
|
||||
/// `hashlib` and `crypt` libraries.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Weak or broken cryptographic hash functions may be susceptible to
|
||||
@@ -43,68 +44,134 @@ use super::super::helpers::string_literal;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `hashlib` — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html)
|
||||
/// - [Python documentation: `crypt` — Function to check Unix passwords](https://docs.python.org/3/library/crypt.html)
|
||||
/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html)
|
||||
/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html)
|
||||
/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html)
|
||||
#[violation]
|
||||
pub struct HashlibInsecureHashFunction {
|
||||
library: String,
|
||||
string: String,
|
||||
}
|
||||
|
||||
impl Violation for HashlibInsecureHashFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let HashlibInsecureHashFunction { string } = self;
|
||||
format!("Probable use of insecure hash functions in `hashlib`: `{string}`")
|
||||
let HashlibInsecureHashFunction { library, string } = self;
|
||||
format!("Probable use of insecure hash functions in `{library}`: `{string}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// S324
|
||||
pub(crate) fn hashlib_insecure_hash_functions(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if let Some(hashlib_call) = checker
|
||||
if let Some(weak_hash_call) = checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.and_then(|qualified_name| match qualified_name.segments() {
|
||||
["hashlib", "new"] => Some(HashlibCall::New),
|
||||
["hashlib", "md4"] => Some(HashlibCall::WeakHash("md4")),
|
||||
["hashlib", "md5"] => Some(HashlibCall::WeakHash("md5")),
|
||||
["hashlib", "sha"] => Some(HashlibCall::WeakHash("sha")),
|
||||
["hashlib", "sha1"] => Some(HashlibCall::WeakHash("sha1")),
|
||||
["hashlib", "new"] => Some(WeakHashCall::Hashlib {
|
||||
call: HashlibCall::New,
|
||||
}),
|
||||
["hashlib", "md4"] => Some(WeakHashCall::Hashlib {
|
||||
call: HashlibCall::WeakHash("md4"),
|
||||
}),
|
||||
["hashlib", "md5"] => Some(WeakHashCall::Hashlib {
|
||||
call: HashlibCall::WeakHash("md5"),
|
||||
}),
|
||||
["hashlib", "sha"] => Some(WeakHashCall::Hashlib {
|
||||
call: HashlibCall::WeakHash("sha"),
|
||||
}),
|
||||
["hashlib", "sha1"] => Some(WeakHashCall::Hashlib {
|
||||
call: HashlibCall::WeakHash("sha1"),
|
||||
}),
|
||||
["crypt", "crypt" | "mksalt"] => Some(WeakHashCall::Crypt),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
if !is_used_for_security(&call.arguments) {
|
||||
return;
|
||||
}
|
||||
match hashlib_call {
|
||||
HashlibCall::New => {
|
||||
if let Some(name_arg) = call.arguments.find_argument("name", 0) {
|
||||
if let Some(hash_func_name) = string_literal(name_arg) {
|
||||
// `hashlib.new` accepts both lowercase and uppercase names for hash
|
||||
// functions.
|
||||
if matches!(
|
||||
hash_func_name,
|
||||
"md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1"
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
string: hash_func_name.to_string(),
|
||||
},
|
||||
name_arg.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
match weak_hash_call {
|
||||
WeakHashCall::Hashlib { call: hashlib_call } => {
|
||||
detect_insecure_hashlib_calls(checker, call, hashlib_call);
|
||||
}
|
||||
HashlibCall::WeakHash(func_name) => {
|
||||
WeakHashCall::Crypt => detect_insecure_crypt_calls(checker, call),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_insecure_hashlib_calls(
|
||||
checker: &mut Checker,
|
||||
call: &ast::ExprCall,
|
||||
hashlib_call: HashlibCall,
|
||||
) {
|
||||
if !is_used_for_security(&call.arguments) {
|
||||
return;
|
||||
}
|
||||
|
||||
match hashlib_call {
|
||||
HashlibCall::New => {
|
||||
let Some(name_arg) = call.arguments.find_argument("name", 0) else {
|
||||
return;
|
||||
};
|
||||
let Some(hash_func_name) = string_literal(name_arg) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// `hashlib.new` accepts both lowercase and uppercase names for hash
|
||||
// functions.
|
||||
if matches!(
|
||||
hash_func_name,
|
||||
"md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1"
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
string: (*func_name).to_string(),
|
||||
library: "hashlib".to_string(),
|
||||
string: hash_func_name.to_string(),
|
||||
},
|
||||
call.func.range(),
|
||||
name_arg.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
HashlibCall::WeakHash(func_name) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
library: "hashlib".to_string(),
|
||||
string: (*func_name).to_string(),
|
||||
},
|
||||
call.func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_insecure_crypt_calls(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(method) = checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.and_then(|qualified_name| match qualified_name.segments() {
|
||||
["crypt", "crypt"] => Some(("salt", 1)),
|
||||
["crypt", "mksalt"] => Some(("method", 0)),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|(argument_name, position)| {
|
||||
call.arguments.find_argument(argument_name, position)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(method) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if matches!(
|
||||
qualified_name.segments(),
|
||||
["crypt", "METHOD_CRYPT" | "METHOD_MD5" | "METHOD_BLOWFISH"]
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
library: "crypt".to_string(),
|
||||
string: qualified_name.to_string(),
|
||||
},
|
||||
method.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +181,13 @@ fn is_used_for_security(arguments: &Arguments) -> bool {
|
||||
.map_or(true, |keyword| !is_const_false(&keyword.value))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum WeakHashCall {
|
||||
Hashlib { call: HashlibCall },
|
||||
Crypt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum HashlibCall {
|
||||
New,
|
||||
WeakHash(&'static str),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use assert_used::*;
|
||||
pub(crate) use bad_file_permissions::*;
|
||||
pub(crate) use django_extra::*;
|
||||
pub(crate) use django_raw_sql::*;
|
||||
pub(crate) use exec_used::*;
|
||||
pub(crate) use flask_debug_true::*;
|
||||
@@ -33,6 +34,7 @@ pub(crate) use weak_cryptographic_key::*;
|
||||
|
||||
mod assert_used;
|
||||
mod bad_file_permissions;
|
||||
mod django_extra;
|
||||
mod django_raw_sql;
|
||||
mod exec_used;
|
||||
mod flask_debug_true;
|
||||
|
||||
@@ -433,6 +433,7 @@ fn get_call_kind(func: &Expr, semantic: &SemanticModel) -> Option<CallKind> {
|
||||
"Popen" | "call" | "check_call" | "check_output" | "run" => {
|
||||
Some(CallKind::Subprocess)
|
||||
}
|
||||
"getoutput" | "getstatusoutput" => Some(CallKind::Shell),
|
||||
_ => None,
|
||||
},
|
||||
"popen2" => match submodule {
|
||||
|
||||
@@ -867,7 +867,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
["urllib", "request", "URLopener" | "FancyURLopener"] |
|
||||
["six", "moves", "urllib", "request", "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.into()),
|
||||
// NonCryptographicRandom
|
||||
["random", "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular"] => Some(SuspiciousNonCryptographicRandomUsage.into()),
|
||||
["random", "Random" | "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular" | "randbytes"] => Some(SuspiciousNonCryptographicRandomUsage.into()),
|
||||
// UnverifiedContext
|
||||
["ssl", "_create_unverified_context"] => Some(SuspiciousUnverifiedContextUsage.into()),
|
||||
// XMLCElementTree
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S311.py:10:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
9 | # Errors
|
||||
10 | random.Random()
|
||||
| ^^^^^^^^^^^^^^^ S311
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
|
|
||||
|
||||
S311.py:11:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
9 | # Errors
|
||||
10 | random.Random()
|
||||
11 | random.random()
|
||||
| ^^^^^^^^^^^^^^^ S311
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
|
|
||||
|
||||
S311.py:12:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
10 | random.Random()
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
| ^^^^^^^^^^^^^^^^^^ S311
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
|
|
||||
|
||||
S311.py:13:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
| ^^^^^^^^^^^^^^^^ S311
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
|
|
||||
|
||||
S311.py:14:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
| ^^^^^^^^^^^^^^^ S311
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
|
|
||||
|
||||
S311.py:15:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
| ^^^^^^^^^^^^^^^^ S311
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
|
|
||||
|
||||
S311.py:16:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
| ^^^^^^^^^^^^^^^^ S311
|
||||
17 | random.triangular()
|
||||
18 | random.randbytes()
|
||||
|
|
||||
|
||||
S311.py:17:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
| ^^^^^^^^^^^^^^^^^^^ S311
|
||||
18 | random.randbytes()
|
||||
|
|
||||
|
||||
S311.py:18:1: S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
|
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
18 | random.randbytes()
|
||||
| ^^^^^^^^^^^^^^^^^^ S311
|
||||
19 |
|
||||
20 | # Unrelated
|
||||
|
|
||||
@@ -3,131 +3,195 @@ source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S324.py:7:13: S324 Probable use of insecure hash functions in `hashlib`: `md5`
|
||||
|
|
||||
5 | # Invalid
|
||||
6 |
|
||||
6 | # Errors
|
||||
7 | hashlib.new('md5')
|
||||
| ^^^^^ S324
|
||||
8 |
|
||||
9 | hashlib.new('md4', b'test')
|
||||
8 | hashlib.new('md4', b'test')
|
||||
9 | hashlib.new(name='md5', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:9:13: S324 Probable use of insecure hash functions in `hashlib`: `md4`
|
||||
S324.py:8:13: S324 Probable use of insecure hash functions in `hashlib`: `md4`
|
||||
|
|
||||
6 | # Errors
|
||||
7 | hashlib.new('md5')
|
||||
8 | hashlib.new('md4', b'test')
|
||||
| ^^^^^ S324
|
||||
9 | hashlib.new(name='md5', data=b'test')
|
||||
10 | hashlib.new('MD4', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:9:18: S324 Probable use of insecure hash functions in `hashlib`: `md5`
|
||||
|
|
||||
7 | hashlib.new('md5')
|
||||
8 |
|
||||
9 | hashlib.new('md4', b'test')
|
||||
| ^^^^^ S324
|
||||
10 |
|
||||
11 | hashlib.new(name='md5', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:11:18: S324 Probable use of insecure hash functions in `hashlib`: `md5`
|
||||
|
|
||||
9 | hashlib.new('md4', b'test')
|
||||
10 |
|
||||
11 | hashlib.new(name='md5', data=b'test')
|
||||
8 | hashlib.new('md4', b'test')
|
||||
9 | hashlib.new(name='md5', data=b'test')
|
||||
| ^^^^^ S324
|
||||
12 |
|
||||
13 | hashlib.new('MD4', data=b'test')
|
||||
10 | hashlib.new('MD4', data=b'test')
|
||||
11 | hashlib.new('sha1')
|
||||
|
|
||||
|
||||
S324.py:13:13: S324 Probable use of insecure hash functions in `hashlib`: `MD4`
|
||||
S324.py:10:13: S324 Probable use of insecure hash functions in `hashlib`: `MD4`
|
||||
|
|
||||
11 | hashlib.new(name='md5', data=b'test')
|
||||
12 |
|
||||
13 | hashlib.new('MD4', data=b'test')
|
||||
8 | hashlib.new('md4', b'test')
|
||||
9 | hashlib.new(name='md5', data=b'test')
|
||||
10 | hashlib.new('MD4', data=b'test')
|
||||
| ^^^^^ S324
|
||||
14 |
|
||||
15 | hashlib.new('sha1')
|
||||
11 | hashlib.new('sha1')
|
||||
12 | hashlib.new('sha1', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:15:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
S324.py:11:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
13 | hashlib.new('MD4', data=b'test')
|
||||
14 |
|
||||
15 | hashlib.new('sha1')
|
||||
9 | hashlib.new(name='md5', data=b'test')
|
||||
10 | hashlib.new('MD4', data=b'test')
|
||||
11 | hashlib.new('sha1')
|
||||
| ^^^^^^ S324
|
||||
16 |
|
||||
17 | hashlib.new('sha1', data=b'test')
|
||||
12 | hashlib.new('sha1', data=b'test')
|
||||
13 | hashlib.new('sha', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:12:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
10 | hashlib.new('MD4', data=b'test')
|
||||
11 | hashlib.new('sha1')
|
||||
12 | hashlib.new('sha1', data=b'test')
|
||||
| ^^^^^^ S324
|
||||
13 | hashlib.new('sha', data=b'test')
|
||||
14 | hashlib.new(name='SHA', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:13:13: S324 Probable use of insecure hash functions in `hashlib`: `sha`
|
||||
|
|
||||
11 | hashlib.new('sha1')
|
||||
12 | hashlib.new('sha1', data=b'test')
|
||||
13 | hashlib.new('sha', data=b'test')
|
||||
| ^^^^^ S324
|
||||
14 | hashlib.new(name='SHA', data=b'test')
|
||||
15 | hashlib.sha(data=b'test')
|
||||
|
|
||||
|
||||
S324.py:14:18: S324 Probable use of insecure hash functions in `hashlib`: `SHA`
|
||||
|
|
||||
12 | hashlib.new('sha1', data=b'test')
|
||||
13 | hashlib.new('sha', data=b'test')
|
||||
14 | hashlib.new(name='SHA', data=b'test')
|
||||
| ^^^^^ S324
|
||||
15 | hashlib.sha(data=b'test')
|
||||
16 | hashlib.md5()
|
||||
|
|
||||
|
||||
S324.py:15:1: S324 Probable use of insecure hash functions in `hashlib`: `sha`
|
||||
|
|
||||
13 | hashlib.new('sha', data=b'test')
|
||||
14 | hashlib.new(name='SHA', data=b'test')
|
||||
15 | hashlib.sha(data=b'test')
|
||||
| ^^^^^^^^^^^ S324
|
||||
16 | hashlib.md5()
|
||||
17 | hashlib_new('sha1')
|
||||
|
|
||||
|
||||
S324.py:16:1: S324 Probable use of insecure hash functions in `hashlib`: `md5`
|
||||
|
|
||||
14 | hashlib.new(name='SHA', data=b'test')
|
||||
15 | hashlib.sha(data=b'test')
|
||||
16 | hashlib.md5()
|
||||
| ^^^^^^^^^^^ S324
|
||||
17 | hashlib_new('sha1')
|
||||
18 | hashlib_sha1('sha1')
|
||||
|
|
||||
|
||||
S324.py:17:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
15 | hashlib.new('sha1')
|
||||
16 |
|
||||
17 | hashlib.new('sha1', data=b'test')
|
||||
15 | hashlib.sha(data=b'test')
|
||||
16 | hashlib.md5()
|
||||
17 | hashlib_new('sha1')
|
||||
| ^^^^^^ S324
|
||||
18 |
|
||||
19 | hashlib.new('sha', data=b'test')
|
||||
18 | hashlib_sha1('sha1')
|
||||
19 | # usedforsecurity arg only available in Python 3.9+
|
||||
|
|
||||
|
||||
S324.py:19:13: S324 Probable use of insecure hash functions in `hashlib`: `sha`
|
||||
S324.py:18:1: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
17 | hashlib.new('sha1', data=b'test')
|
||||
18 |
|
||||
19 | hashlib.new('sha', data=b'test')
|
||||
| ^^^^^ S324
|
||||
20 |
|
||||
21 | hashlib.new(name='SHA', data=b'test')
|
||||
|
|
||||
|
||||
S324.py:21:18: S324 Probable use of insecure hash functions in `hashlib`: `SHA`
|
||||
|
|
||||
19 | hashlib.new('sha', data=b'test')
|
||||
20 |
|
||||
21 | hashlib.new(name='SHA', data=b'test')
|
||||
| ^^^^^ S324
|
||||
22 |
|
||||
23 | hashlib.sha(data=b'test')
|
||||
|
|
||||
|
||||
S324.py:23:1: S324 Probable use of insecure hash functions in `hashlib`: `sha`
|
||||
|
|
||||
21 | hashlib.new(name='SHA', data=b'test')
|
||||
22 |
|
||||
23 | hashlib.sha(data=b'test')
|
||||
| ^^^^^^^^^^^ S324
|
||||
24 |
|
||||
25 | hashlib.md5()
|
||||
|
|
||||
|
||||
S324.py:25:1: S324 Probable use of insecure hash functions in `hashlib`: `md5`
|
||||
|
|
||||
23 | hashlib.sha(data=b'test')
|
||||
24 |
|
||||
25 | hashlib.md5()
|
||||
| ^^^^^^^^^^^ S324
|
||||
26 |
|
||||
27 | hashlib_new('sha1')
|
||||
|
|
||||
|
||||
S324.py:27:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
25 | hashlib.md5()
|
||||
26 |
|
||||
27 | hashlib_new('sha1')
|
||||
| ^^^^^^ S324
|
||||
28 |
|
||||
29 | hashlib_sha1('sha1')
|
||||
|
|
||||
|
||||
S324.py:29:1: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
27 | hashlib_new('sha1')
|
||||
28 |
|
||||
29 | hashlib_sha1('sha1')
|
||||
16 | hashlib.md5()
|
||||
17 | hashlib_new('sha1')
|
||||
18 | hashlib_sha1('sha1')
|
||||
| ^^^^^^^^^^^^ S324
|
||||
30 |
|
||||
31 | # usedforsecurity arg only available in Python 3.9+
|
||||
19 | # usedforsecurity arg only available in Python 3.9+
|
||||
20 | hashlib.new('sha1', usedforsecurity=True)
|
||||
|
|
||||
|
||||
S324.py:32:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
S324.py:20:13: S324 Probable use of insecure hash functions in `hashlib`: `sha1`
|
||||
|
|
||||
31 | # usedforsecurity arg only available in Python 3.9+
|
||||
32 | hashlib.new('sha1', usedforsecurity=True)
|
||||
18 | hashlib_sha1('sha1')
|
||||
19 | # usedforsecurity arg only available in Python 3.9+
|
||||
20 | hashlib.new('sha1', usedforsecurity=True)
|
||||
| ^^^^^^ S324
|
||||
33 |
|
||||
34 | # Valid
|
||||
21 |
|
||||
22 | crypt.crypt("test", salt=crypt.METHOD_CRYPT)
|
||||
|
|
||||
|
||||
S324.py:22:26: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_CRYPT`
|
||||
|
|
||||
20 | hashlib.new('sha1', usedforsecurity=True)
|
||||
21 |
|
||||
22 | crypt.crypt("test", salt=crypt.METHOD_CRYPT)
|
||||
| ^^^^^^^^^^^^^^^^^^ S324
|
||||
23 | crypt.crypt("test", salt=crypt.METHOD_MD5)
|
||||
24 | crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
|
||||
|
|
||||
|
||||
S324.py:23:26: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_MD5`
|
||||
|
|
||||
22 | crypt.crypt("test", salt=crypt.METHOD_CRYPT)
|
||||
23 | crypt.crypt("test", salt=crypt.METHOD_MD5)
|
||||
| ^^^^^^^^^^^^^^^^ S324
|
||||
24 | crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
|
||||
25 | crypt.crypt("test", crypt.METHOD_BLOWFISH)
|
||||
|
|
||||
|
||||
S324.py:24:26: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_BLOWFISH`
|
||||
|
|
||||
22 | crypt.crypt("test", salt=crypt.METHOD_CRYPT)
|
||||
23 | crypt.crypt("test", salt=crypt.METHOD_MD5)
|
||||
24 | crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S324
|
||||
25 | crypt.crypt("test", crypt.METHOD_BLOWFISH)
|
||||
|
|
||||
|
||||
S324.py:25:21: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_BLOWFISH`
|
||||
|
|
||||
23 | crypt.crypt("test", salt=crypt.METHOD_MD5)
|
||||
24 | crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
|
||||
25 | crypt.crypt("test", crypt.METHOD_BLOWFISH)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S324
|
||||
26 |
|
||||
27 | crypt.mksalt(crypt.METHOD_CRYPT)
|
||||
|
|
||||
|
||||
S324.py:27:14: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_CRYPT`
|
||||
|
|
||||
25 | crypt.crypt("test", crypt.METHOD_BLOWFISH)
|
||||
26 |
|
||||
27 | crypt.mksalt(crypt.METHOD_CRYPT)
|
||||
| ^^^^^^^^^^^^^^^^^^ S324
|
||||
28 | crypt.mksalt(crypt.METHOD_MD5)
|
||||
29 | crypt.mksalt(crypt.METHOD_BLOWFISH)
|
||||
|
|
||||
|
||||
S324.py:28:14: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_MD5`
|
||||
|
|
||||
27 | crypt.mksalt(crypt.METHOD_CRYPT)
|
||||
28 | crypt.mksalt(crypt.METHOD_MD5)
|
||||
| ^^^^^^^^^^^^^^^^ S324
|
||||
29 | crypt.mksalt(crypt.METHOD_BLOWFISH)
|
||||
|
|
||||
|
||||
S324.py:29:14: S324 Probable use of insecure hash functions in `crypt`: `crypt.METHOD_BLOWFISH`
|
||||
|
|
||||
27 | crypt.mksalt(crypt.METHOD_CRYPT)
|
||||
28 | crypt.mksalt(crypt.METHOD_MD5)
|
||||
29 | crypt.mksalt(crypt.METHOD_BLOWFISH)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S324
|
||||
30 |
|
||||
31 | # OK
|
||||
|
|
||||
|
||||
@@ -1,147 +1,165 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S605.py:7:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
6 | # Check all shell functions.
|
||||
7 | os.system("true")
|
||||
| ^^^^^^ S605
|
||||
8 | os.popen("true")
|
||||
9 | os.popen2("true")
|
||||
|
|
||||
|
||||
S605.py:8:10: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
S605.py:8:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
6 | # Check all shell functions.
|
||||
7 | os.system("true")
|
||||
8 | os.popen("true")
|
||||
| ^^^^^^ S605
|
||||
9 | os.popen2("true")
|
||||
10 | os.popen3("true")
|
||||
|
|
||||
|
||||
S605.py:9:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
7 | os.system("true")
|
||||
8 | os.popen("true")
|
||||
9 | os.popen2("true")
|
||||
7 | # Check all shell functions.
|
||||
8 | os.system("true")
|
||||
| ^^^^^^ S605
|
||||
10 | os.popen3("true")
|
||||
11 | os.popen4("true")
|
||||
9 | os.popen("true")
|
||||
10 | os.popen2("true")
|
||||
|
|
||||
|
||||
S605.py:9:10: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
7 | # Check all shell functions.
|
||||
8 | os.system("true")
|
||||
9 | os.popen("true")
|
||||
| ^^^^^^ S605
|
||||
10 | os.popen2("true")
|
||||
11 | os.popen3("true")
|
||||
|
|
||||
|
||||
S605.py:10:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
8 | os.popen("true")
|
||||
9 | os.popen2("true")
|
||||
10 | os.popen3("true")
|
||||
8 | os.system("true")
|
||||
9 | os.popen("true")
|
||||
10 | os.popen2("true")
|
||||
| ^^^^^^ S605
|
||||
11 | os.popen4("true")
|
||||
12 | popen2.popen2("true")
|
||||
11 | os.popen3("true")
|
||||
12 | os.popen4("true")
|
||||
|
|
||||
|
||||
S605.py:11:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
9 | os.popen2("true")
|
||||
10 | os.popen3("true")
|
||||
11 | os.popen4("true")
|
||||
9 | os.popen("true")
|
||||
10 | os.popen2("true")
|
||||
11 | os.popen3("true")
|
||||
| ^^^^^^ S605
|
||||
12 | popen2.popen2("true")
|
||||
13 | popen2.popen3("true")
|
||||
12 | os.popen4("true")
|
||||
13 | popen2.popen2("true")
|
||||
|
|
||||
|
||||
S605.py:12:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
S605.py:12:11: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
10 | os.popen3("true")
|
||||
11 | os.popen4("true")
|
||||
12 | popen2.popen2("true")
|
||||
| ^^^^^^ S605
|
||||
13 | popen2.popen3("true")
|
||||
14 | popen2.popen4("true")
|
||||
10 | os.popen2("true")
|
||||
11 | os.popen3("true")
|
||||
12 | os.popen4("true")
|
||||
| ^^^^^^ S605
|
||||
13 | popen2.popen2("true")
|
||||
14 | popen2.popen3("true")
|
||||
|
|
||||
|
||||
S605.py:13:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
11 | os.popen4("true")
|
||||
12 | popen2.popen2("true")
|
||||
13 | popen2.popen3("true")
|
||||
11 | os.popen3("true")
|
||||
12 | os.popen4("true")
|
||||
13 | popen2.popen2("true")
|
||||
| ^^^^^^ S605
|
||||
14 | popen2.popen4("true")
|
||||
15 | popen2.Popen3("true")
|
||||
14 | popen2.popen3("true")
|
||||
15 | popen2.popen4("true")
|
||||
|
|
||||
|
||||
S605.py:14:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
12 | popen2.popen2("true")
|
||||
13 | popen2.popen3("true")
|
||||
14 | popen2.popen4("true")
|
||||
12 | os.popen4("true")
|
||||
13 | popen2.popen2("true")
|
||||
14 | popen2.popen3("true")
|
||||
| ^^^^^^ S605
|
||||
15 | popen2.Popen3("true")
|
||||
16 | popen2.Popen4("true")
|
||||
15 | popen2.popen4("true")
|
||||
16 | popen2.Popen3("true")
|
||||
|
|
||||
|
||||
S605.py:15:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
13 | popen2.popen3("true")
|
||||
14 | popen2.popen4("true")
|
||||
15 | popen2.Popen3("true")
|
||||
13 | popen2.popen2("true")
|
||||
14 | popen2.popen3("true")
|
||||
15 | popen2.popen4("true")
|
||||
| ^^^^^^ S605
|
||||
16 | popen2.Popen4("true")
|
||||
17 | commands.getoutput("true")
|
||||
16 | popen2.Popen3("true")
|
||||
17 | popen2.Popen4("true")
|
||||
|
|
||||
|
||||
S605.py:16:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
14 | popen2.popen4("true")
|
||||
15 | popen2.Popen3("true")
|
||||
16 | popen2.Popen4("true")
|
||||
14 | popen2.popen3("true")
|
||||
15 | popen2.popen4("true")
|
||||
16 | popen2.Popen3("true")
|
||||
| ^^^^^^ S605
|
||||
17 | commands.getoutput("true")
|
||||
18 | commands.getstatusoutput("true")
|
||||
17 | popen2.Popen4("true")
|
||||
18 | commands.getoutput("true")
|
||||
|
|
||||
|
||||
S605.py:17:20: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
S605.py:17:15: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
15 | popen2.Popen3("true")
|
||||
16 | popen2.Popen4("true")
|
||||
17 | commands.getoutput("true")
|
||||
15 | popen2.popen4("true")
|
||||
16 | popen2.Popen3("true")
|
||||
17 | popen2.Popen4("true")
|
||||
| ^^^^^^ S605
|
||||
18 | commands.getoutput("true")
|
||||
19 | commands.getstatusoutput("true")
|
||||
|
|
||||
|
||||
S605.py:18:20: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
16 | popen2.Popen3("true")
|
||||
17 | popen2.Popen4("true")
|
||||
18 | commands.getoutput("true")
|
||||
| ^^^^^^ S605
|
||||
18 | commands.getstatusoutput("true")
|
||||
19 | commands.getstatusoutput("true")
|
||||
20 | subprocess.getoutput("true")
|
||||
|
|
||||
|
||||
S605.py:18:26: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
S605.py:19:26: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
16 | popen2.Popen4("true")
|
||||
17 | commands.getoutput("true")
|
||||
18 | commands.getstatusoutput("true")
|
||||
17 | popen2.Popen4("true")
|
||||
18 | commands.getoutput("true")
|
||||
19 | commands.getstatusoutput("true")
|
||||
| ^^^^^^ S605
|
||||
20 | subprocess.getoutput("true")
|
||||
21 | subprocess.getstatusoutput("true")
|
||||
|
|
||||
|
||||
S605.py:23:11: S605 Starting a process with a shell, possible injection detected
|
||||
S605.py:20:22: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
21 | # Check command argument looks unsafe.
|
||||
22 | var_string = "true"
|
||||
23 | os.system(var_string)
|
||||
18 | commands.getoutput("true")
|
||||
19 | commands.getstatusoutput("true")
|
||||
20 | subprocess.getoutput("true")
|
||||
| ^^^^^^ S605
|
||||
21 | subprocess.getstatusoutput("true")
|
||||
|
|
||||
|
||||
S605.py:21:28: S605 Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`
|
||||
|
|
||||
19 | commands.getstatusoutput("true")
|
||||
20 | subprocess.getoutput("true")
|
||||
21 | subprocess.getstatusoutput("true")
|
||||
| ^^^^^^ S605
|
||||
|
|
||||
|
||||
S605.py:26:11: S605 Starting a process with a shell, possible injection detected
|
||||
|
|
||||
24 | # Check command argument looks unsafe.
|
||||
25 | var_string = "true"
|
||||
26 | os.system(var_string)
|
||||
| ^^^^^^^^^^ S605
|
||||
24 | os.system([var_string])
|
||||
25 | os.system([var_string, ""])
|
||||
27 | os.system([var_string])
|
||||
28 | os.system([var_string, ""])
|
||||
|
|
||||
|
||||
S605.py:24:11: S605 Starting a process with a shell, possible injection detected
|
||||
S605.py:27:11: S605 Starting a process with a shell, possible injection detected
|
||||
|
|
||||
22 | var_string = "true"
|
||||
23 | os.system(var_string)
|
||||
24 | os.system([var_string])
|
||||
25 | var_string = "true"
|
||||
26 | os.system(var_string)
|
||||
27 | os.system([var_string])
|
||||
| ^^^^^^^^^^^^ S605
|
||||
25 | os.system([var_string, ""])
|
||||
28 | os.system([var_string, ""])
|
||||
|
|
||||
|
||||
S605.py:25:11: S605 Starting a process with a shell, possible injection detected
|
||||
S605.py:28:11: S605 Starting a process with a shell, possible injection detected
|
||||
|
|
||||
23 | os.system(var_string)
|
||||
24 | os.system([var_string])
|
||||
25 | os.system([var_string, ""])
|
||||
26 | os.system(var_string)
|
||||
27 | os.system([var_string])
|
||||
28 | os.system([var_string, ""])
|
||||
| ^^^^^^^^^^^^^^^^ S605
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S610.py:4:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
3 | # Errors
|
||||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
|
||||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
|
|
||||
|
||||
S610.py:5:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
3 | # Errors
|
||||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure'))
|
||||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
|
|
||||
|
||||
S610.py:6:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure'))
|
||||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
|
||||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
|
||||
|
|
||||
|
||||
S610.py:7:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
|
||||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
|
||||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
|
||||
|
|
||||
|
||||
S610.py:8:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
|
||||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
|
||||
|
|
||||
|
||||
S610.py:9:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
|
||||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
|
||||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
10 |
|
||||
11 | query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
|
||||
|
|
||||
|
||||
S610.py:12:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
11 | query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
|
||||
12 | User.objects.filter(username='admin').extra(select={'test': query})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
13 |
|
||||
14 | where_var = ['1=1) OR 1=1 AND (1=1']
|
||||
|
|
||||
|
||||
S610.py:15:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
14 | where_var = ['1=1) OR 1=1 AND (1=1']
|
||||
15 | User.objects.filter(username='admin').extra(where=where_var)
|
||||
| ^^^^^^^^^^^^^^^^^ S610
|
||||
16 |
|
||||
17 | where_str = '1=1) OR 1=1 AND (1=1'
|
||||
|
|
||||
|
||||
S610.py:18:44: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
17 | where_str = '1=1) OR 1=1 AND (1=1'
|
||||
18 | User.objects.filter(username='admin').extra(where=[where_str])
|
||||
| ^^^^^^^^^^^^^^^^^^^ S610
|
||||
19 |
|
||||
20 | tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
|
||||
|
|
||||
|
||||
S610.py:21:25: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
20 | tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
|
||||
21 | User.objects.all().extra(tables=tables_var).distinct()
|
||||
| ^^^^^^^^^^^^^^^^^^^ S610
|
||||
22 |
|
||||
23 | tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
|
||||
|
|
||||
|
||||
S610.py:24:25: S610 Use of Django `extra` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
23 | tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
|
||||
24 | User.objects.all().extra(tables=[tables_str]).distinct()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S610
|
||||
25 |
|
||||
26 | # OK
|
||||
|
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, parameter: &Para
|
||||
BuiltinArgumentShadowing {
|
||||
name: parameter.name.to_string(),
|
||||
},
|
||||
parameter.range(),
|
||||
parameter.name.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ pub(crate) fn trailing_commas(
|
||||
// F-strings are handled as `String` token type with the complete range
|
||||
// of the outermost f-string. This means that the expression inside the
|
||||
// f-string is not checked for trailing commas.
|
||||
Tok::FStringStart => {
|
||||
Tok::FStringStart(_) => {
|
||||
fstrings = fstrings.saturating_add(1);
|
||||
None
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::helpers;
|
||||
///
|
||||
/// If a list literal was passed, then it should be rewritten as a `tuple`
|
||||
/// literal. Otherwise, if a tuple literal was passed, then the outer call
|
||||
/// to `list()` should be removed.
|
||||
/// to `tuple()` should be removed.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
|
||||
@@ -205,7 +205,7 @@ C413.py:14:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
14 |+sorted((i for i in range(42)), reverse=True)
|
||||
15 15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
16 16 |
|
||||
17 17 |
|
||||
17 17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
|
||||
C413.py:15:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
@@ -213,6 +213,8 @@ C413.py:15:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
14 | reversed(sorted(i for i in range(42)))
|
||||
15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
16 |
|
||||
17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
@@ -223,7 +225,38 @@ C413.py:15:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
15 |-reversed(sorted((i for i in range(42)), reverse=True))
|
||||
15 |+sorted((i for i in range(42)), reverse=False)
|
||||
16 16 |
|
||||
17 17 |
|
||||
18 18 | def reversed(*args, **kwargs):
|
||||
17 17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
18 18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
|
||||
C413.py:18:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
19 | reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
15 15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
16 16 |
|
||||
17 17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
18 |-reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
18 |+sorted([1, 2, 3], reverse=not (False or True))
|
||||
19 19 | reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
|
||||
C413.py:19:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
19 | reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 |
|
||||
17 17 | # Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
18 18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
19 |-reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
19 |+sorted([1, 2, 3], reverse=not (False or True))
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_python_ast::{Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder};
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -69,10 +69,7 @@ pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
/// Checks for the presence of a debugger import.
|
||||
pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<Diagnostic> {
|
||||
if let Some(module) = module {
|
||||
let mut builder =
|
||||
QualifiedNameBuilder::from_qualified_name(QualifiedName::imported(module));
|
||||
builder.push(name);
|
||||
let qualified_name = builder.build();
|
||||
let qualified_name = QualifiedName::user_defined(module).append_member(name);
|
||||
|
||||
if is_debugger_call(&qualified_name) {
|
||||
return Some(Diagnostic::new(
|
||||
@@ -83,7 +80,7 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) ->
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let qualified_name = QualifiedName::imported(name);
|
||||
let qualified_name = QualifiedName::user_defined(name);
|
||||
|
||||
if is_debugger_import(&qualified_name) {
|
||||
return Some(Diagnostic::new(
|
||||
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn implicit(
|
||||
{
|
||||
let (a_range, b_range) = match (a_tok, b_tok) {
|
||||
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
|
||||
(Tok::String { .. }, Tok::FStringStart) => {
|
||||
(Tok::String { .. }, Tok::FStringStart(_)) => {
|
||||
match indexer.fstring_ranges().innermost(b_range.start()) {
|
||||
Some(b_range) => (*a_range, b_range),
|
||||
None => continue,
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn implicit(
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
(Tok::FStringEnd, Tok::FStringStart) => {
|
||||
(Tok::FStringEnd, Tok::FStringStart(_)) => {
|
||||
match (
|
||||
indexer.fstring_ranges().innermost(a_range.start()),
|
||||
indexer.fstring_ranges().innermost(b_range.start()),
|
||||
|
||||
@@ -11,7 +11,7 @@ mod tests {
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_import_conventions::settings::default_aliases;
|
||||
use crate::rules::flake8_import_conventions::settings::{default_aliases, BannedAliases};
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
|
||||
@@ -57,17 +57,20 @@ mod tests {
|
||||
banned_aliases: FxHashMap::from_iter([
|
||||
(
|
||||
"typing".to_string(),
|
||||
vec!["t".to_string(), "ty".to_string()],
|
||||
BannedAliases::from_iter(["t".to_string(), "ty".to_string()]),
|
||||
),
|
||||
(
|
||||
"numpy".to_string(),
|
||||
vec!["nmp".to_string(), "npy".to_string()],
|
||||
BannedAliases::from_iter(["nmp".to_string(), "npy".to_string()]),
|
||||
),
|
||||
(
|
||||
"tensorflow.keras.backend".to_string(),
|
||||
vec!["K".to_string()],
|
||||
BannedAliases::from_iter(["K".to_string()]),
|
||||
),
|
||||
(
|
||||
"torch.nn.functional".to_string(),
|
||||
BannedAliases::from_iter(["F".to_string()]),
|
||||
),
|
||||
("torch.nn.functional".to_string(), vec!["F".to_string()]),
|
||||
]),
|
||||
banned_from: FxHashSet::default(),
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use ruff_python_ast::Stmt;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::rules::flake8_import_conventions::settings::BannedAliases;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports that use non-standard naming conventions, like
|
||||
/// `import tensorflow.keras.backend as K`.
|
||||
@@ -49,7 +51,7 @@ pub(crate) fn banned_import_alias(
|
||||
stmt: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
banned_conventions: &FxHashMap<String, Vec<String>>,
|
||||
banned_conventions: &FxHashMap<String, BannedAliases>,
|
||||
) -> Option<Diagnostic> {
|
||||
if let Some(banned_aliases) = banned_conventions.get(name) {
|
||||
if banned_aliases
|
||||
|
||||
@@ -65,7 +65,7 @@ pub(crate) fn unconventional_import_alias(
|
||||
return None;
|
||||
};
|
||||
|
||||
let qualified_name = import.qualified_name();
|
||||
let qualified_name = import.qualified_name().to_string();
|
||||
|
||||
let Some(expected_alias) = conventions.get(qualified_name.as_str()) else {
|
||||
return None;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//! Settings for import conventions.
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::display_settings;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
use crate::display_settings;
|
||||
|
||||
const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
|
||||
("altair", "alt"),
|
||||
("matplotlib", "mpl"),
|
||||
@@ -23,10 +26,41 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
|
||||
("pyarrow", "pa"),
|
||||
];
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct BannedAliases(Vec<String>);
|
||||
|
||||
impl Display for BannedAliases {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[")?;
|
||||
for (i, alias) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{alias}")?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
|
||||
impl BannedAliases {
|
||||
/// Returns an iterator over the banned aliases.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.0.iter().map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<String> for BannedAliases {
|
||||
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, CacheKey)]
|
||||
pub struct Settings {
|
||||
pub aliases: FxHashMap<String, String>,
|
||||
pub banned_aliases: FxHashMap<String, Vec<String>>,
|
||||
pub banned_aliases: FxHashMap<String, BannedAliases>,
|
||||
pub banned_from: FxHashSet<String>,
|
||||
}
|
||||
|
||||
@@ -53,9 +87,9 @@ impl Display for Settings {
|
||||
formatter = f,
|
||||
namespace = "linter.flake8_import_conventions",
|
||||
fields = [
|
||||
self.aliases | debug,
|
||||
self.banned_aliases | debug,
|
||||
self.banned_from | array,
|
||||
self.aliases | map,
|
||||
self.banned_aliases | map,
|
||||
self.banned_from | set,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -61,7 +61,10 @@ pub(crate) fn unaliased_collections_abc_set_import(
|
||||
let BindingKind::FromImport(import) = &binding.kind else {
|
||||
return None;
|
||||
};
|
||||
if !matches!(import.call_path(), ["collections", "abc", "Set"]) {
|
||||
if !matches!(
|
||||
import.qualified_name().segments(),
|
||||
["collections", "abc", "Set"]
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ use super::helpers::is_empty_or_null_string;
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(MyError):
|
||||
/// setup() # may raise `MyError`
|
||||
/// func_to_test()
|
||||
/// setup()
|
||||
/// func_to_test() # not executed if `setup()` raises `MyError`
|
||||
/// assert foo() # not executed
|
||||
/// ```
|
||||
///
|
||||
@@ -38,7 +38,7 @@ use super::helpers::is_empty_or_null_string;
|
||||
///
|
||||
/// def test_foo():
|
||||
/// setup()
|
||||
/// with pytest.raises(MyException):
|
||||
/// with pytest.raises(MyError):
|
||||
/// func_to_test()
|
||||
/// assert foo()
|
||||
/// ```
|
||||
|
||||
@@ -413,5 +413,3 @@ PT018.py:65:5: PT018 [*] Assertion should be broken down into multiple parts
|
||||
70 72 |
|
||||
71 73 | assert (not self.find_graph_output(node.output[0]) or
|
||||
72 74 | self.find_graph_input(node.input[0]))
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::str::{is_triple_quote, leading_quote};
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -158,7 +157,7 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
// ```python
|
||||
// f'"foo" {'nested'}"
|
||||
// ```
|
||||
if matches!(tok, Tok::String { .. } | Tok::FStringStart) {
|
||||
if matches!(tok, Tok::String { .. } | Tok::FStringStart(_)) {
|
||||
if let Some(fstring_context) = fstrings.last_mut() {
|
||||
fstring_context.ignore_escaped_quotes();
|
||||
continue;
|
||||
@@ -170,16 +169,13 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
Tok::String {
|
||||
value: string_contents,
|
||||
kind,
|
||||
triple_quoted,
|
||||
} => {
|
||||
if kind.is_raw() || *triple_quoted {
|
||||
if kind.is_raw_string() || kind.is_triple_quoted() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we're using the preferred quotation style.
|
||||
if !leading_quote(locator.slice(tok_range)).is_some_and(|text| {
|
||||
contains_quote(text, quotes_settings.inline_quotes.as_char())
|
||||
}) {
|
||||
if Quote::from(kind.quote_style()) != quotes_settings.inline_quotes {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -192,7 +188,7 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, tok_range);
|
||||
let fixed_contents = format!(
|
||||
"{prefix}{quote}{value}{quote}",
|
||||
prefix = kind.as_str(),
|
||||
prefix = kind.prefix_str(),
|
||||
quote = quotes_settings.inline_quotes.opposite().as_char(),
|
||||
value = unescape_string(
|
||||
string_contents,
|
||||
@@ -206,12 +202,11 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Tok::FStringStart => {
|
||||
let text = locator.slice(tok_range);
|
||||
Tok::FStringStart(kind) => {
|
||||
// Check for escaped quote only if we're using the preferred quotation
|
||||
// style and it isn't a triple-quoted f-string.
|
||||
let check_for_escaped_quote = !is_triple_quote(text)
|
||||
&& contains_quote(text, quotes_settings.inline_quotes.as_char());
|
||||
let check_for_escaped_quote = !kind.is_triple_quoted()
|
||||
&& Quote::from(kind.quote_style()) == quotes_settings.inline_quotes;
|
||||
fstrings.push(FStringContext::new(
|
||||
check_for_escaped_quote,
|
||||
tok_range,
|
||||
@@ -220,9 +215,8 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
}
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
kind,
|
||||
} if !kind.is_raw_string() => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
@@ -315,17 +309,12 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
Tok::String {
|
||||
value: string_contents,
|
||||
kind,
|
||||
triple_quoted,
|
||||
} => {
|
||||
if kind.is_raw() || *triple_quoted {
|
||||
if kind.is_raw_string() || kind.is_triple_quoted() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let leading = match leading_quote(locator.slice(tok_range)) {
|
||||
Some("\"") => Quote::Double,
|
||||
Some("'") => Quote::Single,
|
||||
_ => continue,
|
||||
};
|
||||
let leading = kind.quote_style();
|
||||
if !contains_escaped_quote(string_contents, leading.opposite().as_char()) {
|
||||
continue;
|
||||
}
|
||||
@@ -333,7 +322,7 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, tok_range);
|
||||
let fixed_contents = format!(
|
||||
"{prefix}{quote}{value}{quote}",
|
||||
prefix = kind.as_str(),
|
||||
prefix = kind.prefix_str(),
|
||||
quote = leading.as_char(),
|
||||
value = unescape_string(string_contents, leading.opposite().as_char())
|
||||
);
|
||||
@@ -343,16 +332,11 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
)));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
Tok::FStringStart => {
|
||||
let text = locator.slice(tok_range);
|
||||
Tok::FStringStart(kind) => {
|
||||
// Check for escaped quote only if we're using the preferred quotation
|
||||
// style and it isn't a triple-quoted f-string.
|
||||
let check_for_escaped_quote = !is_triple_quote(text);
|
||||
let quote_style = if contains_quote(text, Quote::Single.as_char()) {
|
||||
Quote::Single
|
||||
} else {
|
||||
Quote::Double
|
||||
};
|
||||
let check_for_escaped_quote = !kind.is_triple_quoted();
|
||||
let quote_style = Quote::from(kind.quote_style());
|
||||
fstrings.push(FStringContext::new(
|
||||
check_for_escaped_quote,
|
||||
tok_range,
|
||||
@@ -361,9 +345,8 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
}
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
kind,
|
||||
} if !kind.is_raw_string() => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -383,7 +383,7 @@ struct FStringRangeBuilder {
|
||||
impl FStringRangeBuilder {
|
||||
fn visit_token(&mut self, token: &Tok, range: TextRange) {
|
||||
match token {
|
||||
Tok::FStringStart => {
|
||||
Tok::FStringStart(_) => {
|
||||
if self.nesting == 0 {
|
||||
self.start_location = range.start();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ impl Default for Quote {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruff_python_ast::str::QuoteStyle> for Quote {
|
||||
fn from(value: ruff_python_ast::str::QuoteStyle) -> Self {
|
||||
match value {
|
||||
ruff_python_ast::str::QuoteStyle::Double => Self::Double,
|
||||
ruff_python_ast::str::QuoteStyle::Single => Self::Single,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, CacheKey)]
|
||||
pub struct Settings {
|
||||
pub inline_quotes: Quote,
|
||||
|
||||
@@ -326,8 +326,10 @@ singles_escaped_unnecessary.py:43:26: Q004 [*] Unnecessary escape on inner quote
|
||||
|
|
||||
41 | # Make sure we do not unescape quotes
|
||||
42 | this_is_fine = "This is an \\'escaped\\' quote"
|
||||
43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash"
|
||||
43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004
|
||||
44 |
|
||||
45 | # Invalid escapes in bytestrings are also triggered:
|
||||
|
|
||||
= help: Remove backslash
|
||||
|
||||
@@ -335,7 +337,23 @@ singles_escaped_unnecessary.py:43:26: Q004 [*] Unnecessary escape on inner quote
|
||||
40 40 |
|
||||
41 41 | # Make sure we do not unescape quotes
|
||||
42 42 | this_is_fine = "This is an \\'escaped\\' quote"
|
||||
43 |-this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash"
|
||||
43 |+this_should_raise_Q004 = "This is an \\'escaped\\' quote with an extra backslash"
|
||||
43 |-this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
|
||||
43 |+this_should_raise_Q004 = "This is an \\'escaped\\' quote with an extra backslash" # Q004
|
||||
44 44 |
|
||||
45 45 | # Invalid escapes in bytestrings are also triggered:
|
||||
46 46 | x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
|
||||
|
||||
singles_escaped_unnecessary.py:46:5: Q004 [*] Unnecessary escape on inner quote character
|
||||
|
|
||||
45 | # Invalid escapes in bytestrings are also triggered:
|
||||
46 | x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004
|
||||
|
|
||||
= help: Remove backslash
|
||||
|
||||
ℹ Safe fix
|
||||
43 43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
|
||||
44 44 |
|
||||
45 45 | # Invalid escapes in bytestrings are also triggered:
|
||||
46 |-x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
|
||||
46 |+x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce'rm\x0e\xcd\xe9.\xf8\xd2" # Q004
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ast::{StringLiteralFlags, StringLiteralPrefix};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -218,7 +219,13 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
|
||||
);
|
||||
let node = ast::StringLiteral {
|
||||
value: capital_env_var.into_boxed_str(),
|
||||
unicode: env_var.is_unicode(),
|
||||
flags: StringLiteralFlags::default().with_prefix({
|
||||
if env_var.is_unicode() {
|
||||
StringLiteralPrefix::UString
|
||||
} else {
|
||||
StringLiteralPrefix::None
|
||||
}
|
||||
}),
|
||||
..ast::StringLiteral::default()
|
||||
};
|
||||
let new_env_var = node.into();
|
||||
|
||||
@@ -13,6 +13,12 @@ pub struct ApiBan {
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
impl Display for ApiBan {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, Default)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -47,7 +53,7 @@ impl Display for Settings {
|
||||
namespace = "linter.flake8_tidy_imports",
|
||||
fields = [
|
||||
self.ban_relative_imports,
|
||||
self.banned_api | debug,
|
||||
self.banned_api | map,
|
||||
self.banned_module_level_imports | array,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RuntimeImportInTypeCheckingBlock {
|
||||
qualified_name: import.qualified_name(),
|
||||
qualified_name: import.qualified_name().to_string(),
|
||||
strategy: Strategy::MoveImport,
|
||||
},
|
||||
range,
|
||||
@@ -218,7 +218,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RuntimeImportInTypeCheckingBlock {
|
||||
qualified_name: import.qualified_name(),
|
||||
qualified_name: import.qualified_name().to_string(),
|
||||
strategy: Strategy::QuoteUsages,
|
||||
},
|
||||
range,
|
||||
@@ -245,7 +245,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RuntimeImportInTypeCheckingBlock {
|
||||
qualified_name: import.qualified_name(),
|
||||
qualified_name: import.qualified_name().to_string(),
|
||||
strategy: Strategy::MoveImport,
|
||||
},
|
||||
range,
|
||||
|
||||
@@ -282,7 +282,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
let qualified_name = import.qualified_name();
|
||||
|
||||
if is_exempt(
|
||||
qualified_name.as_str(),
|
||||
&qualified_name.to_string(),
|
||||
&checker
|
||||
.settings
|
||||
.flake8_type_checking
|
||||
@@ -296,7 +296,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
|
||||
// Categorize the import, using coarse-grained categorization.
|
||||
let import_type = match categorize(
|
||||
qualified_name.as_str(),
|
||||
&qualified_name.to_string(),
|
||||
None,
|
||||
&checker.settings.src,
|
||||
checker.package(),
|
||||
@@ -365,8 +365,10 @@ pub(crate) fn typing_only_runtime_import(
|
||||
..
|
||||
} in imports
|
||||
{
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(diagnostic_for(import_type, import.qualified_name()), range);
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
diagnostic_for(import_type, import.qualified_name().to_string()),
|
||||
range,
|
||||
);
|
||||
if let Some(range) = parent_range {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
@@ -387,8 +389,10 @@ pub(crate) fn typing_only_runtime_import(
|
||||
..
|
||||
} in imports
|
||||
{
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(diagnostic_for(import_type, import.qualified_name()), range);
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
diagnostic_for(import_type, import.qualified_name().to_string()),
|
||||
range,
|
||||
);
|
||||
if let Some(range) = parent_range {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ast::FStringFlags;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::fix::edits::pad;
|
||||
@@ -97,6 +98,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
|
||||
let node = ast::FString {
|
||||
elements: f_string_elements,
|
||||
range: TextRange::default(),
|
||||
flags: FStringFlags::default(),
|
||||
};
|
||||
Some(node.into())
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use test_case::test_case;
|
||||
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -495,7 +495,7 @@ mod tests {
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
force_to_top: BTreeSet::from([
|
||||
force_to_top: FxHashSet::from_iter([
|
||||
"z".to_string(),
|
||||
"lib1".to_string(),
|
||||
"lib3".to_string(),
|
||||
@@ -575,9 +575,10 @@ mod tests {
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
force_single_line: true,
|
||||
single_line_exclusions: vec!["os".to_string(), "logging.handlers".to_string()]
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
single_line_exclusions: FxHashSet::from_iter([
|
||||
"os".to_string(),
|
||||
"logging.handlers".to_string(),
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
@@ -636,7 +637,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
order_by_type: true,
|
||||
classes: BTreeSet::from([
|
||||
classes: FxHashSet::from_iter([
|
||||
"SVC".to_string(),
|
||||
"SELU".to_string(),
|
||||
"N_CLASS".to_string(),
|
||||
@@ -664,7 +665,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
order_by_type: true,
|
||||
constants: BTreeSet::from([
|
||||
constants: FxHashSet::from_iter([
|
||||
"Const".to_string(),
|
||||
"constant".to_string(),
|
||||
"First".to_string(),
|
||||
@@ -694,7 +695,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
order_by_type: true,
|
||||
variables: BTreeSet::from([
|
||||
variables: FxHashSet::from_iter([
|
||||
"VAR".to_string(),
|
||||
"Variable".to_string(),
|
||||
"MyVar".to_string(),
|
||||
@@ -721,7 +722,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
force_sort_within_sections: true,
|
||||
force_to_top: BTreeSet::from(["z".to_string()]),
|
||||
force_to_top: FxHashSet::from_iter(["z".to_string()]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
@@ -771,7 +772,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
required_imports: BTreeSet::from([
|
||||
required_imports: BTreeSet::from_iter([
|
||||
"from __future__ import annotations".to_string()
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
@@ -801,7 +802,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
required_imports: BTreeSet::from([
|
||||
required_imports: BTreeSet::from_iter([
|
||||
"from __future__ import annotations as _annotations".to_string(),
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
@@ -824,7 +825,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
required_imports: BTreeSet::from([
|
||||
required_imports: BTreeSet::from_iter([
|
||||
"from __future__ import annotations".to_string(),
|
||||
"from __future__ import generator_stop".to_string(),
|
||||
]),
|
||||
@@ -848,7 +849,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
required_imports: BTreeSet::from(["from __future__ import annotations, \
|
||||
required_imports: BTreeSet::from_iter(["from __future__ import annotations, \
|
||||
generator_stop"
|
||||
.to_string()]),
|
||||
..super::settings::Settings::default()
|
||||
@@ -871,7 +872,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
required_imports: BTreeSet::from(["import os".to_string()]),
|
||||
required_imports: BTreeSet::from_iter(["import os".to_string()]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::MissingRequiredImport)
|
||||
@@ -1002,7 +1003,7 @@ mod tests {
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
no_lines_before: BTreeSet::from([
|
||||
no_lines_before: FxHashSet::from_iter([
|
||||
ImportSection::Known(ImportType::Future),
|
||||
ImportSection::Known(ImportType::StandardLibrary),
|
||||
ImportSection::Known(ImportType::ThirdParty),
|
||||
@@ -1030,7 +1031,7 @@ mod tests {
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
isort: super::settings::Settings {
|
||||
no_lines_before: BTreeSet::from([
|
||||
no_lines_before: FxHashSet::from_iter([
|
||||
ImportSection::Known(ImportType::StandardLibrary),
|
||||
ImportSection::Known(ImportType::LocalFolder),
|
||||
]),
|
||||
|
||||
@@ -5,12 +5,13 @@ use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::display_settings;
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
use crate::display_settings;
|
||||
use crate::rules::isort::categorize::KnownModules;
|
||||
use crate::rules::isort::ImportType;
|
||||
|
||||
@@ -52,17 +53,17 @@ pub struct Settings {
|
||||
pub force_sort_within_sections: bool,
|
||||
pub case_sensitive: bool,
|
||||
pub force_wrap_aliases: bool,
|
||||
pub force_to_top: BTreeSet<String>,
|
||||
pub force_to_top: FxHashSet<String>,
|
||||
pub known_modules: KnownModules,
|
||||
pub detect_same_package: bool,
|
||||
pub order_by_type: bool,
|
||||
pub relative_imports_order: RelativeImportsOrder,
|
||||
pub single_line_exclusions: BTreeSet<String>,
|
||||
pub single_line_exclusions: FxHashSet<String>,
|
||||
pub split_on_trailing_comma: bool,
|
||||
pub classes: BTreeSet<String>,
|
||||
pub constants: BTreeSet<String>,
|
||||
pub variables: BTreeSet<String>,
|
||||
pub no_lines_before: BTreeSet<ImportSection>,
|
||||
pub classes: FxHashSet<String>,
|
||||
pub constants: FxHashSet<String>,
|
||||
pub variables: FxHashSet<String>,
|
||||
pub no_lines_before: FxHashSet<ImportSection>,
|
||||
pub lines_after_imports: isize,
|
||||
pub lines_between_types: usize,
|
||||
pub forced_separate: Vec<String>,
|
||||
@@ -77,23 +78,23 @@ pub struct Settings {
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
required_imports: BTreeSet::new(),
|
||||
required_imports: BTreeSet::default(),
|
||||
combine_as_imports: false,
|
||||
force_single_line: false,
|
||||
force_sort_within_sections: false,
|
||||
detect_same_package: true,
|
||||
case_sensitive: false,
|
||||
force_wrap_aliases: false,
|
||||
force_to_top: BTreeSet::new(),
|
||||
force_to_top: FxHashSet::default(),
|
||||
known_modules: KnownModules::default(),
|
||||
order_by_type: true,
|
||||
relative_imports_order: RelativeImportsOrder::default(),
|
||||
single_line_exclusions: BTreeSet::new(),
|
||||
single_line_exclusions: FxHashSet::default(),
|
||||
split_on_trailing_comma: true,
|
||||
classes: BTreeSet::new(),
|
||||
constants: BTreeSet::new(),
|
||||
variables: BTreeSet::new(),
|
||||
no_lines_before: BTreeSet::new(),
|
||||
classes: FxHashSet::default(),
|
||||
constants: FxHashSet::default(),
|
||||
variables: FxHashSet::default(),
|
||||
no_lines_before: FxHashSet::default(),
|
||||
lines_after_imports: -1,
|
||||
lines_between_types: 0,
|
||||
forced_separate: Vec::new(),
|
||||
@@ -113,23 +114,23 @@ impl Display for Settings {
|
||||
formatter = f,
|
||||
namespace = "linter.isort",
|
||||
fields = [
|
||||
self.required_imports | array,
|
||||
self.required_imports | set,
|
||||
self.combine_as_imports,
|
||||
self.force_single_line,
|
||||
self.force_sort_within_sections,
|
||||
self.detect_same_package,
|
||||
self.case_sensitive,
|
||||
self.force_wrap_aliases,
|
||||
self.force_to_top | array,
|
||||
self.force_to_top | set,
|
||||
self.known_modules,
|
||||
self.order_by_type,
|
||||
self.relative_imports_order,
|
||||
self.single_line_exclusions | array,
|
||||
self.single_line_exclusions | set,
|
||||
self.split_on_trailing_comma,
|
||||
self.classes | array,
|
||||
self.constants | array,
|
||||
self.variables | array,
|
||||
self.no_lines_before | array,
|
||||
self.classes | set,
|
||||
self.constants | set,
|
||||
self.variables | set,
|
||||
self.no_lines_before | set,
|
||||
self.lines_after_imports,
|
||||
self.lines_between_types,
|
||||
self.forced_separate | array,
|
||||
@@ -155,7 +156,7 @@ pub enum SettingsError {
|
||||
InvalidUserDefinedSection(glob::PatternError),
|
||||
}
|
||||
|
||||
impl fmt::Display for SettingsError {
|
||||
impl Display for SettingsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SettingsError::InvalidKnownThirdParty(err) => {
|
||||
|
||||
@@ -50,7 +50,9 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti
|
||||
| BindingKind::ComprehensionVar
|
||||
| BindingKind::Global
|
||||
| BindingKind::Nonlocal(_) => Resolution::RelevantLocal,
|
||||
BindingKind::Import(import) if matches!(import.call_path(), ["pandas"]) => {
|
||||
BindingKind::Import(import)
|
||||
if matches!(import.qualified_name().segments(), ["pandas"]) =>
|
||||
{
|
||||
Resolution::PandasModule
|
||||
}
|
||||
_ => Resolution::IrrelevantBinding,
|
||||
|
||||
@@ -16,7 +16,7 @@ mod tests {
|
||||
|
||||
use crate::line_width::LineLength;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pycodestyle;
|
||||
use crate::rules::{isort, pycodestyle};
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
@@ -71,6 +71,12 @@ mod tests {
|
||||
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
|
||||
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
|
||||
#[test_case(Rule::RedundantBackslash, Path::new("E502.py"))]
|
||||
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))]
|
||||
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))]
|
||||
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_2.py"))]
|
||||
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_3.py"))]
|
||||
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_4.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
@@ -138,13 +144,6 @@ mod tests {
|
||||
Path::new("E25.py")
|
||||
)]
|
||||
#[test_case(Rule::MissingWhitespaceAroundParameterEquals, Path::new("E25.py"))]
|
||||
#[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLineAfterDecorator, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesAfterFunctionOrClass, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesBeforeNestedDefinition, Path::new("E30.py"))]
|
||||
|
||||
fn logical(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
@@ -155,6 +154,129 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the compatibility of E2 rules (E202, E225 and E275) on syntactically incorrect code.
|
||||
#[test]
|
||||
fn white_space_syntax_error_compatibility() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join("E2_syntax_error.py"),
|
||||
&settings::LinterSettings {
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::MissingWhitespaceAroundOperator,
|
||||
Rule::MissingWhitespaceAfterKeyword,
|
||||
Rule::WhitespaceBeforeCloseBracket,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))]
|
||||
#[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLineAfterDecorator, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesAfterFunctionOrClass, Path::new("E30.py"))]
|
||||
#[test_case(Rule::BlankLinesBeforeNestedDefinition, Path::new("E30.py"))]
|
||||
fn blank_lines(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the compatibility of the blank line top level rule and isort.
|
||||
#[test_case(-1, 0)]
|
||||
#[test_case(1, 1)]
|
||||
#[test_case(0, 0)]
|
||||
#[test_case(4, 4)]
|
||||
fn blank_lines_top_level_isort_compatibility(
|
||||
lines_after_imports: isize,
|
||||
lines_between_types: usize,
|
||||
) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"blank_lines_top_level_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})"
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join("E30_isort.py"),
|
||||
&settings::LinterSettings {
|
||||
isort: isort::settings::Settings {
|
||||
lines_after_imports,
|
||||
lines_between_types,
|
||||
..isort::settings::Settings::default()
|
||||
},
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::BlankLinesTopLevel,
|
||||
Rule::UnsortedImports,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the compatibility of the blank line too many lines and isort.
|
||||
#[test_case(-1, 0)]
|
||||
#[test_case(1, 1)]
|
||||
#[test_case(0, 0)]
|
||||
#[test_case(4, 4)]
|
||||
fn too_many_blank_lines_isort_compatibility(
|
||||
lines_after_imports: isize,
|
||||
lines_between_types: usize,
|
||||
) -> Result<()> {
|
||||
let snapshot = format!("too_many_blank_lines_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})");
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join("E30_isort.py"),
|
||||
&settings::LinterSettings {
|
||||
isort: isort::settings::Settings {
|
||||
lines_after_imports,
|
||||
lines_between_types,
|
||||
..isort::settings::Settings::default()
|
||||
},
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::TooManyBlankLines,
|
||||
Rule::UnsortedImports,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BlankLineBetweenMethods)]
|
||||
#[test_case(Rule::BlankLinesTopLevel)]
|
||||
#[test_case(Rule::TooManyBlankLines)]
|
||||
#[test_case(Rule::BlankLineAfterDecorator)]
|
||||
#[test_case(Rule::BlankLinesAfterFunctionOrClass)]
|
||||
#[test_case(Rule::BlankLinesBeforeNestedDefinition)]
|
||||
fn blank_lines_typing_stub(rule_code: Rule) -> Result<()> {
|
||||
let snapshot = format!("blank_lines_{}_typing_stub", rule_code.noqa_code());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join("E30.pyi"),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_lines_typing_stub_isort() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join("E30_isort.pyi"),
|
||||
&settings::LinterSettings {
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::TooManyBlankLines,
|
||||
Rule::BlankLinesTopLevel,
|
||||
Rule::UnsortedImports,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_literals() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -8,6 +8,7 @@ use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::lexer::LexicalError;
|
||||
@@ -24,7 +25,7 @@ use ruff_python_trivia::PythonWhitespace;
|
||||
/// Number of blank lines around top level classes and functions.
|
||||
const BLANK_LINES_TOP_LEVEL: u32 = 2;
|
||||
/// Number of blank lines around methods and nested classes and functions.
|
||||
const BLANK_LINES_METHOD_LEVEL: u32 = 1;
|
||||
const BLANK_LINES_NESTED_LEVEL: u32 = 1;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for missing blank lines between methods of a class.
|
||||
@@ -51,16 +52,21 @@ const BLANK_LINES_METHOD_LEVEL: u32 = 1;
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Typing stub files (`.pyi`)
|
||||
/// The typing style guide recommends to not use blank lines between methods except to group
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
pub struct BlankLineBetweenMethods;
|
||||
|
||||
impl AlwaysFixableViolation for BlankLineBetweenMethods {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Expected {BLANK_LINES_METHOD_LEVEL:?} blank line, found 0")
|
||||
format!("Expected {BLANK_LINES_NESTED_LEVEL:?} blank line, found 0")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
@@ -74,6 +80,10 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
|
||||
/// ## Why is this bad?
|
||||
/// PEP 8 recommends exactly two blank lines between top level functions and classes.
|
||||
///
|
||||
/// Note: The rule respects the [`lint.isort.lines-after-imports`] setting when determining
|
||||
/// the required number of blank lines between top-level `import` statements and function or class definitions
|
||||
/// for compatibility with isort.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def func1():
|
||||
@@ -92,22 +102,29 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Typing stub files (`.pyi`)
|
||||
/// The typing style guide recommends to not use blank lines between classes and functions except to group
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
pub struct BlankLinesTopLevel {
|
||||
actual_blank_lines: u32,
|
||||
expected_blank_lines: u32,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for BlankLinesTopLevel {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BlankLinesTopLevel {
|
||||
actual_blank_lines: nb_blank_lines,
|
||||
actual_blank_lines,
|
||||
expected_blank_lines,
|
||||
} = self;
|
||||
|
||||
format!("Expected {BLANK_LINES_TOP_LEVEL:?} blank lines, found {nb_blank_lines}")
|
||||
format!("Expected {expected_blank_lines:?} blank lines, found {actual_blank_lines}")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
@@ -144,9 +161,17 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Typing stub files (`.pyi`)
|
||||
/// The rule allows at most one blank line in typing stub files in accordance to the typing style guide recommendation.
|
||||
///
|
||||
/// Note: The rule respects the following `isort` settings when determining the maximum number of blank lines allowed between two statements:
|
||||
/// * [`lint.isort.lines-after-imports`]: For top-level statements directly following an import statement.
|
||||
/// * [`lint.isort.lines-between-types`]: For `import` statements directly following a `from ... import ...` statement or vice versa.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
pub struct TooManyBlankLines {
|
||||
actual_blank_lines: u32,
|
||||
@@ -155,10 +180,9 @@ pub struct TooManyBlankLines {
|
||||
impl AlwaysFixableViolation for TooManyBlankLines {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TooManyBlankLines {
|
||||
actual_blank_lines: nb_blank_lines,
|
||||
} = self;
|
||||
format!("Too many blank lines ({nb_blank_lines})")
|
||||
let TooManyBlankLines { actual_blank_lines } = self;
|
||||
|
||||
format!("Too many blank lines ({actual_blank_lines})")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
@@ -237,9 +261,14 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
|
||||
/// user = User()
|
||||
/// ```
|
||||
///
|
||||
/// ## Typing stub files (`.pyi`)
|
||||
/// The typing style guide recommends to not use blank lines between statements except to group
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
pub struct BlankLinesAfterFunctionOrClass {
|
||||
actual_blank_lines: u32,
|
||||
@@ -286,9 +315,14 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Typing stub files (`.pyi`)
|
||||
/// The typing style guide recommends to not use blank lines between classes and functions except to group
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
pub struct BlankLinesBeforeNestedDefinition;
|
||||
|
||||
@@ -415,6 +449,8 @@ impl<'a> Iterator for LinePreprocessor<'a> {
|
||||
{
|
||||
LogicalLineKind::Function
|
||||
}
|
||||
TokenKind::Import => LogicalLineKind::Import,
|
||||
TokenKind::From => LogicalLineKind::FromImport,
|
||||
_ => LogicalLineKind::Other,
|
||||
};
|
||||
|
||||
@@ -560,9 +596,17 @@ enum Follows {
|
||||
Other,
|
||||
Decorator,
|
||||
Def,
|
||||
Import,
|
||||
FromImport,
|
||||
Docstring,
|
||||
}
|
||||
|
||||
impl Follows {
|
||||
const fn is_any_import(self) -> bool {
|
||||
matches!(self, Follows::Import | Follows::FromImport)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
enum Status {
|
||||
/// Stores the indent level where the nesting started.
|
||||
@@ -602,8 +646,351 @@ impl Status {
|
||||
}
|
||||
|
||||
/// Contains variables used for the linting of blank lines.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct BlankLinesChecker {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BlankLinesChecker<'a> {
|
||||
stylist: &'a Stylist<'a>,
|
||||
locator: &'a Locator<'a>,
|
||||
indent_width: IndentWidth,
|
||||
lines_after_imports: isize,
|
||||
lines_between_types: usize,
|
||||
source_type: PySourceType,
|
||||
}
|
||||
|
||||
impl<'a> BlankLinesChecker<'a> {
|
||||
pub(crate) fn new(
|
||||
locator: &'a Locator<'a>,
|
||||
stylist: &'a Stylist<'a>,
|
||||
settings: &crate::settings::LinterSettings,
|
||||
source_type: PySourceType,
|
||||
) -> BlankLinesChecker<'a> {
|
||||
BlankLinesChecker {
|
||||
stylist,
|
||||
locator,
|
||||
indent_width: settings.tab_size,
|
||||
lines_after_imports: settings.isort.lines_after_imports,
|
||||
lines_between_types: settings.isort.lines_between_types,
|
||||
source_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// E301, E302, E303, E304, E305, E306
|
||||
pub(crate) fn check_lines(&self, tokens: &[LexResult], diagnostics: &mut Vec<Diagnostic>) {
|
||||
let mut prev_indent_length: Option<usize> = None;
|
||||
let mut state = BlankLinesState::default();
|
||||
let line_preprocessor = LinePreprocessor::new(tokens, self.locator, self.indent_width);
|
||||
|
||||
for logical_line in line_preprocessor {
|
||||
// Reset `follows` after a dedent:
|
||||
// ```python
|
||||
// if True:
|
||||
// import test
|
||||
// a = 10
|
||||
// ```
|
||||
// The `a` statement doesn't follow the `import` statement but the `if` statement.
|
||||
if let Some(prev_indent_length) = prev_indent_length {
|
||||
if prev_indent_length > logical_line.indent_length {
|
||||
state.follows = Follows::Other;
|
||||
}
|
||||
}
|
||||
|
||||
state.class_status.update(&logical_line);
|
||||
state.fn_status.update(&logical_line);
|
||||
|
||||
if state.is_not_first_logical_line {
|
||||
self.check_line(&logical_line, &state, prev_indent_length, diagnostics);
|
||||
}
|
||||
|
||||
match logical_line.kind {
|
||||
LogicalLineKind::Class => {
|
||||
if matches!(state.class_status, Status::Outside) {
|
||||
state.class_status = Status::Inside(logical_line.indent_length);
|
||||
}
|
||||
state.follows = Follows::Other;
|
||||
}
|
||||
LogicalLineKind::Decorator => {
|
||||
state.follows = Follows::Decorator;
|
||||
}
|
||||
LogicalLineKind::Function => {
|
||||
if matches!(state.fn_status, Status::Outside) {
|
||||
state.fn_status = Status::Inside(logical_line.indent_length);
|
||||
}
|
||||
state.follows = Follows::Def;
|
||||
}
|
||||
LogicalLineKind::Comment => {}
|
||||
LogicalLineKind::Import => {
|
||||
state.follows = Follows::Import;
|
||||
}
|
||||
LogicalLineKind::FromImport => {
|
||||
state.follows = Follows::FromImport;
|
||||
}
|
||||
LogicalLineKind::Other => {
|
||||
state.follows = Follows::Other;
|
||||
}
|
||||
}
|
||||
|
||||
if logical_line.is_docstring {
|
||||
state.follows = Follows::Docstring;
|
||||
}
|
||||
|
||||
if !logical_line.is_comment_only {
|
||||
state.is_not_first_logical_line = true;
|
||||
|
||||
state.last_non_comment_line_end = logical_line.logical_line_end;
|
||||
|
||||
if logical_line.indent_length == 0 {
|
||||
state.previous_unindented_line_kind = Some(logical_line.kind);
|
||||
}
|
||||
}
|
||||
|
||||
if !logical_line.is_comment_only {
|
||||
prev_indent_length = Some(logical_line.indent_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
fn check_line(
|
||||
&self,
|
||||
line: &LogicalLineInfo,
|
||||
state: &BlankLinesState,
|
||||
prev_indent_length: Option<usize>,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
if line.preceding_blank_lines == 0
|
||||
// Only applies to methods.
|
||||
&& matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(state.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
|
||||
&& matches!(state.class_status, Status::Inside(_))
|
||||
// The class/parent method's docstring can directly precede the def.
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(state.follows, Follows::Docstring | Follows::Decorator)
|
||||
// Do not trigger when the def follows an if/while/etc...
|
||||
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
|
||||
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
|
||||
&& !self.source_type.is_stub()
|
||||
{
|
||||
// E301
|
||||
let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
self.stylist.line_ending().to_string(),
|
||||
self.locator.line_start(state.last_non_comment_line_end),
|
||||
)));
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
|
||||
let max_lines_level = if self.source_type.is_stub() {
|
||||
1
|
||||
} else {
|
||||
if line.indent_length == 0 {
|
||||
BLANK_LINES_TOP_LEVEL
|
||||
} else {
|
||||
BLANK_LINES_NESTED_LEVEL
|
||||
}
|
||||
};
|
||||
|
||||
let expected_blank_lines_before_definition = if line.indent_length == 0 {
|
||||
// Mimic the isort rules for the number of blank lines before classes and functions
|
||||
if state.follows.is_any_import() {
|
||||
// Fallback to the default if the value is too large for an u32 or if it is negative.
|
||||
// A negative value means that isort should determine the blank lines automatically.
|
||||
// `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise.
|
||||
// Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the
|
||||
// blank lines before a class or function definition.
|
||||
u32::try_from(self.lines_after_imports).unwrap_or(max_lines_level)
|
||||
} else {
|
||||
max_lines_level
|
||||
}
|
||||
} else {
|
||||
max_lines_level
|
||||
};
|
||||
|
||||
if line.preceding_blank_lines < expected_blank_lines_before_definition
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(state.follows, Follows::Decorator)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(state.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
|
||||
// Only trigger on non-indented classes and functions (for example functions within an if are ignored)
|
||||
&& line.indent_length == 0
|
||||
// Only apply to functions or classes.
|
||||
&& line.kind.is_class_function_or_decorator()
|
||||
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
|
||||
&& !self.source_type.is_stub()
|
||||
{
|
||||
// E302
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesTopLevel {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
expected_blank_lines: expected_blank_lines_before_definition,
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
self.stylist
|
||||
.line_ending()
|
||||
.repeat(expected_blank_lines_before_definition as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
self.stylist
|
||||
.line_ending()
|
||||
.repeat(expected_blank_lines_before_definition as usize),
|
||||
self.locator.line_start(state.last_non_comment_line_end),
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
// If between `import` and `from .. import ..` or the other way round,
|
||||
// allow up to `lines_between_types` newlines for isort compatibility.
|
||||
// We let `isort` remove extra blank lines when the imports belong
|
||||
// to different sections.
|
||||
let max_blank_lines = if matches!(
|
||||
(line.kind, state.follows),
|
||||
(LogicalLineKind::Import, Follows::FromImport)
|
||||
| (LogicalLineKind::FromImport, Follows::Import)
|
||||
) {
|
||||
max_lines_level.max(u32::try_from(self.lines_between_types).unwrap_or(u32::MAX))
|
||||
} else {
|
||||
expected_blank_lines_before_definition
|
||||
};
|
||||
|
||||
if line.blank_lines > max_blank_lines {
|
||||
// E303
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
TooManyBlankLines {
|
||||
actual_blank_lines: line.blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
if max_blank_lines == 0 {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(blank_lines_range)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
self.stylist.line_ending().repeat(max_blank_lines as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if matches!(state.follows, Follows::Decorator)
|
||||
&& !line.is_comment_only
|
||||
&& line.preceding_blank_lines > 0
|
||||
{
|
||||
// E304
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLineAfterDecorator {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
// Get all the lines between the last decorator line (included) and the current line (included).
|
||||
// Then remove all blank lines.
|
||||
let trivia_range = TextRange::new(
|
||||
state.last_non_comment_line_end,
|
||||
self.locator.line_start(line.first_token_range.start()),
|
||||
);
|
||||
let trivia_text = self.locator.slice(trivia_range);
|
||||
let mut trivia_without_blank_lines = trivia_text
|
||||
.universal_newlines()
|
||||
.filter_map(|line| (!line.trim_whitespace().is_empty()).then_some(line.as_str()))
|
||||
.join(&self.stylist.line_ending());
|
||||
|
||||
let fix = if trivia_without_blank_lines.is_empty() {
|
||||
Fix::safe_edit(Edit::range_deletion(trivia_range))
|
||||
} else {
|
||||
trivia_without_blank_lines.push_str(&self.stylist.line_ending());
|
||||
Fix::safe_edit(Edit::range_replacement(
|
||||
trivia_without_blank_lines,
|
||||
trivia_range,
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic.set_fix(fix);
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
|
||||
&& state
|
||||
.previous_unindented_line_kind
|
||||
.is_some_and(LogicalLineKind::is_class_function_or_decorator)
|
||||
&& line.indent_length == 0
|
||||
&& !line.is_comment_only
|
||||
&& !line.kind.is_class_function_or_decorator()
|
||||
// Blank lines in stub files are used for grouping, don't enforce blank lines.
|
||||
&& !self.source_type.is_stub()
|
||||
{
|
||||
// E305
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesAfterFunctionOrClass {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
self.stylist
|
||||
.line_ending()
|
||||
.repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
self.stylist
|
||||
.line_ending()
|
||||
.repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
self.locator.line_start(line.first_token_range.start()),
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if line.preceding_blank_lines == 0
|
||||
// Only apply to nested functions.
|
||||
&& matches!(state.fn_status, Status::Inside(_))
|
||||
&& line.kind.is_class_function_or_decorator()
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(state.follows, Follows::Decorator)
|
||||
// The class's docstring can directly precede the first function.
|
||||
&& !matches!(state.follows, Follows::Docstring)
|
||||
// Do not trigger when the def/class follows an "indenting token" (if/while/etc...).
|
||||
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(state.follows, Follows::Def) && line.last_token != TokenKind::Colon)
|
||||
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
|
||||
&& !self.source_type.is_stub()
|
||||
{
|
||||
// E306
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range);
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
self.stylist.line_ending().to_string(),
|
||||
self.locator.line_start(line.first_token_range.start()),
|
||||
)));
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct BlankLinesState {
|
||||
follows: Follows,
|
||||
fn_status: Status,
|
||||
class_status: Status,
|
||||
@@ -615,264 +1002,6 @@ pub(crate) struct BlankLinesChecker {
|
||||
previous_unindented_line_kind: Option<LogicalLineKind>,
|
||||
}
|
||||
|
||||
impl BlankLinesChecker {
|
||||
/// E301, E302, E303, E304, E305, E306
|
||||
pub(crate) fn check_lines(
|
||||
&mut self,
|
||||
tokens: &[LexResult],
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
indent_width: IndentWidth,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let mut prev_indent_length: Option<usize> = None;
|
||||
let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width);
|
||||
|
||||
for logical_line in line_preprocessor {
|
||||
self.check_line(
|
||||
&logical_line,
|
||||
prev_indent_length,
|
||||
locator,
|
||||
stylist,
|
||||
diagnostics,
|
||||
);
|
||||
if !logical_line.is_comment_only {
|
||||
prev_indent_length = Some(logical_line.indent_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
fn check_line(
|
||||
&mut self,
|
||||
line: &LogicalLineInfo,
|
||||
prev_indent_length: Option<usize>,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
self.class_status.update(line);
|
||||
self.fn_status.update(line);
|
||||
|
||||
// Don't expect blank lines before the first non comment line.
|
||||
if self.is_not_first_logical_line {
|
||||
if line.preceding_blank_lines == 0
|
||||
// Only applies to methods.
|
||||
&& matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
|
||||
&& matches!(self.class_status, Status::Inside(_))
|
||||
// The class/parent method's docstring can directly precede the def.
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(self.follows, Follows::Docstring | Follows::Decorator)
|
||||
// Do not trigger when the def follows an if/while/etc...
|
||||
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
|
||||
{
|
||||
// E301
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
stylist.line_ending().to_string(),
|
||||
locator.line_start(self.last_non_comment_line_end),
|
||||
)));
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(self.follows, Follows::Decorator)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
|
||||
// Only trigger on non-indented classes and functions (for example functions within an if are ignored)
|
||||
&& line.indent_length == 0
|
||||
// Only apply to functions or classes.
|
||||
&& line.kind.is_top_level()
|
||||
{
|
||||
// E302
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesTopLevel {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
locator.line_start(self.last_non_comment_line_end),
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
let expected_blank_lines = if line.indent_length > 0 {
|
||||
BLANK_LINES_METHOD_LEVEL
|
||||
} else {
|
||||
BLANK_LINES_TOP_LEVEL
|
||||
};
|
||||
|
||||
if line.blank_lines > expected_blank_lines {
|
||||
// E303
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
TooManyBlankLines {
|
||||
actual_blank_lines: line.blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
stylist.line_ending().repeat(expected_blank_lines as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if matches!(self.follows, Follows::Decorator)
|
||||
&& !line.is_comment_only
|
||||
&& line.preceding_blank_lines > 0
|
||||
{
|
||||
// E304
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLineAfterDecorator {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
// Get all the lines between the last decorator line (included) and the current line (included).
|
||||
// Then remove all blank lines.
|
||||
let trivia_range = TextRange::new(
|
||||
self.last_non_comment_line_end,
|
||||
locator.line_start(line.first_token_range.start()),
|
||||
);
|
||||
let trivia_text = locator.slice(trivia_range);
|
||||
let mut trivia_without_blank_lines = trivia_text
|
||||
.universal_newlines()
|
||||
.filter_map(|line| {
|
||||
(!line.trim_whitespace().is_empty()).then_some(line.as_str())
|
||||
})
|
||||
.join(&stylist.line_ending());
|
||||
|
||||
let fix = if trivia_without_blank_lines.is_empty() {
|
||||
Fix::safe_edit(Edit::range_deletion(trivia_range))
|
||||
} else {
|
||||
trivia_without_blank_lines.push_str(&stylist.line_ending());
|
||||
Fix::safe_edit(Edit::range_replacement(
|
||||
trivia_without_blank_lines,
|
||||
trivia_range,
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic.set_fix(fix);
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
|
||||
&& self
|
||||
.previous_unindented_line_kind
|
||||
.is_some_and(LogicalLineKind::is_top_level)
|
||||
&& line.indent_length == 0
|
||||
&& !line.is_comment_only
|
||||
&& !line.kind.is_top_level()
|
||||
{
|
||||
// E305
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesAfterFunctionOrClass {
|
||||
actual_blank_lines: line.preceding_blank_lines.count(),
|
||||
},
|
||||
line.first_token_range,
|
||||
);
|
||||
|
||||
if let Some(blank_lines_range) = line.blank_lines.range() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
blank_lines_range,
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
locator.line_start(line.first_token_range.start()),
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if line.preceding_blank_lines == 0
|
||||
// Only apply to nested functions.
|
||||
&& matches!(self.fn_status, Status::Inside(_))
|
||||
&& line.kind.is_top_level()
|
||||
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
|
||||
&& !matches!(self.follows, Follows::Decorator)
|
||||
// The class's docstring can directly precede the first function.
|
||||
&& !matches!(self.follows, Follows::Docstring)
|
||||
// Do not trigger when the def/class follows an "indenting token" (if/while/etc...).
|
||||
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
|
||||
// Allow groups of one-liners.
|
||||
&& !(matches!(self.follows, Follows::Def) && line.last_token != TokenKind::Colon)
|
||||
{
|
||||
// E306
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range);
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
stylist.line_ending().to_string(),
|
||||
locator.line_start(line.first_token_range.start()),
|
||||
)));
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
match line.kind {
|
||||
LogicalLineKind::Class => {
|
||||
if matches!(self.class_status, Status::Outside) {
|
||||
self.class_status = Status::Inside(line.indent_length);
|
||||
}
|
||||
self.follows = Follows::Other;
|
||||
}
|
||||
LogicalLineKind::Decorator => {
|
||||
self.follows = Follows::Decorator;
|
||||
}
|
||||
LogicalLineKind::Function => {
|
||||
if matches!(self.fn_status, Status::Outside) {
|
||||
self.fn_status = Status::Inside(line.indent_length);
|
||||
}
|
||||
self.follows = Follows::Def;
|
||||
}
|
||||
LogicalLineKind::Comment => {}
|
||||
LogicalLineKind::Other => {
|
||||
self.follows = Follows::Other;
|
||||
}
|
||||
}
|
||||
|
||||
if line.is_docstring {
|
||||
self.follows = Follows::Docstring;
|
||||
}
|
||||
|
||||
if !line.is_comment_only {
|
||||
self.is_not_first_logical_line = true;
|
||||
|
||||
self.last_non_comment_line_end = line.logical_line_end;
|
||||
|
||||
if line.indent_length == 0 {
|
||||
self.previous_unindented_line_kind = Some(line.kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum LogicalLineKind {
|
||||
/// The clause header of a class definition
|
||||
@@ -883,12 +1012,16 @@ enum LogicalLineKind {
|
||||
Function,
|
||||
/// A comment only line
|
||||
Comment,
|
||||
/// An import statement
|
||||
Import,
|
||||
/// A from.. import statement
|
||||
FromImport,
|
||||
/// Any other statement or clause header
|
||||
Other,
|
||||
}
|
||||
|
||||
impl LogicalLineKind {
|
||||
fn is_top_level(self) -> bool {
|
||||
fn is_class_function_or_decorator(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
LogicalLineKind::Class | LogicalLineKind::Function | LogicalLineKind::Decorator
|
||||
|
||||
@@ -3,7 +3,7 @@ use memchr::memchr_iter;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{StringKind, Tok};
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
@@ -66,21 +66,21 @@ pub(crate) fn invalid_escape_sequence(
|
||||
token: &Tok,
|
||||
token_range: TextRange,
|
||||
) {
|
||||
let (token_source_code, string_start_location) = match token {
|
||||
Tok::FStringMiddle { value, is_raw, .. } => {
|
||||
if *is_raw {
|
||||
let (token_source_code, string_start_location, kind) = match token {
|
||||
Tok::FStringMiddle { value, kind } => {
|
||||
if kind.is_raw_string() {
|
||||
return;
|
||||
}
|
||||
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
|
||||
return;
|
||||
};
|
||||
(&**value, range.start())
|
||||
(&**value, range.start(), kind)
|
||||
}
|
||||
Tok::String { kind, .. } => {
|
||||
if kind.is_raw() {
|
||||
if kind.is_raw_string() {
|
||||
return;
|
||||
}
|
||||
(locator.slice(token_range), token_range.start())
|
||||
(locator.slice(token_range), token_range.start(), kind)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
@@ -207,13 +207,7 @@ pub(crate) fn invalid_escape_sequence(
|
||||
invalid_escape_char.range(),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
token,
|
||||
Tok::String {
|
||||
kind: StringKind::Unicode,
|
||||
..
|
||||
}
|
||||
) {
|
||||
if kind.is_u_string() {
|
||||
// Replace the Unicode prefix with `r`.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||
"r".to_string(),
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum EqCmpOp {
|
||||
@@ -71,59 +72,83 @@ impl AlwaysFixableViolation for NoneComparison {
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for comparisons to booleans which are not using the `is` operator.
|
||||
/// Checks for equality comparisons to boolean literals.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// According to [PEP 8], "Comparisons to singletons like None should always be done with
|
||||
/// is or is not, never the equality operators."
|
||||
/// [PEP 8] recommends against using the equality operators `==` and `!=` to
|
||||
/// compare values to `True` or `False`.
|
||||
///
|
||||
/// Instead, use `if cond:` or `if not cond:` to check for truth values.
|
||||
///
|
||||
/// If you intend to check if a value is the boolean literal `True` or `False`,
|
||||
/// consider using `is` or `is not` to check for identity instead.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if arg == True:
|
||||
/// pass
|
||||
/// if False == arg:
|
||||
/// pass
|
||||
/// if foo == True:
|
||||
/// ...
|
||||
///
|
||||
/// if bar == False:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// if arg is True:
|
||||
/// pass
|
||||
/// if arg is False:
|
||||
/// pass
|
||||
/// if foo:
|
||||
/// ...
|
||||
///
|
||||
/// if not bar:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations
|
||||
#[violation]
|
||||
pub struct TrueFalseComparison(bool, EqCmpOp);
|
||||
pub struct TrueFalseComparison {
|
||||
value: bool,
|
||||
op: EqCmpOp,
|
||||
cond: Option<SourceCodeSnippet>,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for TrueFalseComparison {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TrueFalseComparison(value, op) = self;
|
||||
let TrueFalseComparison { value, op, cond } = self;
|
||||
let Some(cond) = cond else {
|
||||
return "Avoid equality comparisons to `True` or `False`".to_string();
|
||||
};
|
||||
let cond = cond.truncated_display();
|
||||
match (value, op) {
|
||||
(true, EqCmpOp::Eq) => {
|
||||
format!("Comparison to `True` should be `cond is True` or `if cond:`")
|
||||
format!("Avoid equality comparisons to `True`; use `if {cond}:` for truth checks")
|
||||
}
|
||||
(true, EqCmpOp::NotEq) => {
|
||||
format!("Comparison to `True` should be `cond is not True` or `if not cond:`")
|
||||
format!(
|
||||
"Avoid inequality comparisons to `True`; use `if not {cond}:` for false checks"
|
||||
)
|
||||
}
|
||||
(false, EqCmpOp::Eq) => {
|
||||
format!("Comparison to `False` should be `cond is False` or `if not cond:`")
|
||||
format!(
|
||||
"Avoid equality comparisons to `False`; use `if not {cond}:` for false checks"
|
||||
)
|
||||
}
|
||||
(false, EqCmpOp::NotEq) => {
|
||||
format!("Comparison to `False` should be `cond is not False` or `if cond:`")
|
||||
format!(
|
||||
"Avoid inequality comparisons to `False`; use `if {cond}:` for truth checks"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let TrueFalseComparison(value, op) = self;
|
||||
let TrueFalseComparison { value, op, cond } = self;
|
||||
let Some(cond) = cond.as_ref().and_then(|cond| cond.full_display()) else {
|
||||
return "Replace comparison".to_string();
|
||||
};
|
||||
match (value, op) {
|
||||
(true, EqCmpOp::Eq) => "Replace with `cond is True`".to_string(),
|
||||
(true, EqCmpOp::NotEq) => "Replace with `cond is not True`".to_string(),
|
||||
(false, EqCmpOp::Eq) => "Replace with `cond is False`".to_string(),
|
||||
(false, EqCmpOp::NotEq) => "Replace with `cond is not False`".to_string(),
|
||||
(true, EqCmpOp::Eq) => format!("Replace with `{cond}`"),
|
||||
(true, EqCmpOp::NotEq) => format!("Replace with `not {cond}`"),
|
||||
(false, EqCmpOp::Eq) => format!("Replace with `not {cond}`"),
|
||||
(false, EqCmpOp::NotEq) => format!("Replace with `{cond}`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,17 +192,35 @@ pub(crate) fn literal_comparisons(checker: &mut Checker, compare: &ast::ExprComp
|
||||
if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) = comparator {
|
||||
match op {
|
||||
EqCmpOp::Eq => {
|
||||
let cond = if compare.ops.len() == 1 {
|
||||
Some(SourceCodeSnippet::from_str(checker.locator().slice(next)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostic = Diagnostic::new(
|
||||
TrueFalseComparison(*value, op),
|
||||
comparator.range(),
|
||||
TrueFalseComparison {
|
||||
value: *value,
|
||||
op,
|
||||
cond,
|
||||
},
|
||||
compare.range(),
|
||||
);
|
||||
bad_ops.insert(0, CmpOp::Is);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
EqCmpOp::NotEq => {
|
||||
let cond = if compare.ops.len() == 1 {
|
||||
Some(SourceCodeSnippet::from_str(checker.locator().slice(next)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostic = Diagnostic::new(
|
||||
TrueFalseComparison(*value, op),
|
||||
comparator.range(),
|
||||
TrueFalseComparison {
|
||||
value: *value,
|
||||
op,
|
||||
cond,
|
||||
},
|
||||
compare.range(),
|
||||
);
|
||||
bad_ops.insert(0, CmpOp::IsNot);
|
||||
diagnostics.push(diagnostic);
|
||||
@@ -220,14 +263,40 @@ pub(crate) fn literal_comparisons(checker: &mut Checker, compare: &ast::ExprComp
|
||||
if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) = next {
|
||||
match op {
|
||||
EqCmpOp::Eq => {
|
||||
let diagnostic =
|
||||
Diagnostic::new(TrueFalseComparison(*value, op), next.range());
|
||||
let cond = if compare.ops.len() == 1 {
|
||||
Some(SourceCodeSnippet::from_str(
|
||||
checker.locator().slice(comparator),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostic = Diagnostic::new(
|
||||
TrueFalseComparison {
|
||||
value: *value,
|
||||
op,
|
||||
cond,
|
||||
},
|
||||
compare.range(),
|
||||
);
|
||||
bad_ops.insert(index, CmpOp::Is);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
EqCmpOp::NotEq => {
|
||||
let diagnostic =
|
||||
Diagnostic::new(TrueFalseComparison(*value, op), next.range());
|
||||
let cond = if compare.ops.len() == 1 {
|
||||
Some(SourceCodeSnippet::from_str(
|
||||
checker.locator().slice(comparator),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostic = Diagnostic::new(
|
||||
TrueFalseComparison {
|
||||
value: *value,
|
||||
op,
|
||||
cond,
|
||||
},
|
||||
compare.range(),
|
||||
);
|
||||
bad_ops.insert(index, CmpOp::IsNot);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -137,16 +137,16 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin
|
||||
match kind {
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
TokenKind::Lsqb if fstrings == 0 => {
|
||||
TokenKind::Lsqb => {
|
||||
brackets.push(kind);
|
||||
}
|
||||
TokenKind::Rsqb if fstrings == 0 => {
|
||||
TokenKind::Rsqb => {
|
||||
brackets.pop();
|
||||
}
|
||||
TokenKind::Lbrace if fstrings == 0 => {
|
||||
TokenKind::Lbrace => {
|
||||
brackets.push(kind);
|
||||
}
|
||||
TokenKind::Rbrace if fstrings == 0 => {
|
||||
TokenKind::Rbrace => {
|
||||
brackets.pop();
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -59,7 +59,13 @@ pub(crate) fn missing_whitespace_after_keyword(
|
||||
|| tok0_kind == TokenKind::Yield && tok1_kind == TokenKind::Rpar
|
||||
|| matches!(
|
||||
tok1_kind,
|
||||
TokenKind::Colon | TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||
TokenKind::Colon
|
||||
| TokenKind::Newline
|
||||
| TokenKind::NonLogicalNewline
|
||||
// In the event of a syntax error, do not attempt to add a whitespace.
|
||||
| TokenKind::Rpar
|
||||
| TokenKind::Rsqb
|
||||
| TokenKind::Rbrace
|
||||
))
|
||||
&& tok0.end() == tok1.start()
|
||||
{
|
||||
|
||||
@@ -211,6 +211,21 @@ pub(crate) fn missing_whitespace_around_operator(
|
||||
} else {
|
||||
NeedsSpace::No
|
||||
}
|
||||
} else if tokens.peek().is_some_and(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace
|
||||
)
|
||||
}) {
|
||||
// There should not be a closing bracket directly after a token, as it is a syntax
|
||||
// error. For example:
|
||||
// ```
|
||||
// 1+)
|
||||
// ```
|
||||
//
|
||||
// However, allow it in order to prevent entering an infinite loop in which E225 adds a
|
||||
// space only for E202 to remove it.
|
||||
NeedsSpace::No
|
||||
} else if is_whitespace_needed(kind) {
|
||||
NeedsSpace::Yes
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ pub(crate) use indentation::*;
|
||||
pub(crate) use missing_whitespace::*;
|
||||
pub(crate) use missing_whitespace_after_keyword::*;
|
||||
pub(crate) use missing_whitespace_around_operator::*;
|
||||
pub(crate) use redundant_backslash::*;
|
||||
pub(crate) use space_around_operator::*;
|
||||
pub(crate) use whitespace_around_keywords::*;
|
||||
pub(crate) use whitespace_around_named_parameter_equals::*;
|
||||
@@ -25,6 +26,7 @@ mod indentation;
|
||||
mod missing_whitespace;
|
||||
mod missing_whitespace_after_keyword;
|
||||
mod missing_whitespace_around_operator;
|
||||
mod redundant_backslash;
|
||||
mod space_around_operator;
|
||||
mod whitespace_around_keywords;
|
||||
mod whitespace_around_named_parameter_equals;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user