Compare commits

..

78 Commits

Author SHA1 Message Date
Micha Reiser
98f29bdcec Discard changes to crates/ruff_python_formatter/src/lib.rs 2023-10-27 17:09:07 -05:00
Micha Reiser
215b1c9f25 Split tuples in return positions by comma first 2023-10-27 17:09:07 -05:00
Zanie
88bf2ac43f Lint 2023-10-27 15:48:21 -05:00
Zanie
16e511a734 Use FormatOptions; exclude file from demisto/content with syntax error 2023-10-27 15:46:33 -05:00
Zanie
7d5122603e Show a different summary if there are only errors 2023-10-27 15:39:49 -05:00
Zanie
7d4b59d6cb Merge branch 'main' into zanie/ecosystem-format 2023-10-27 15:16:20 -05:00
Zanie
ee1883ac6c Fix missing space 2023-10-27 13:28:09 -05:00
Zanie
0c1a7ffbdc Fix chmod 2023-10-27 13:27:40 -05:00
Zanie
1337c4ad2a Add docstrings for formatting 2023-10-27 12:49:29 -05:00
Zanie
c3354ad843 Lint 2023-10-27 12:48:14 -05:00
Zanie
b6066c6bef Fix missing newline 2023-10-27 12:41:17 -05:00
Zanie
eb769ac03d Improve handling of ruff executable lookup 2023-10-27 12:40:42 -05:00
Zanie
39c04c8335 Fix spacing in examples 2023-10-27 12:30:49 -05:00
Zanie
5f28912a0d Remove format ignore lines re 2023-10-27 12:29:36 -05:00
Zanie
05ab316609 Rename cache variable; update clone type 2023-10-27 12:29:03 -05:00
Zanie
a16c04c6db Fix limited_parallelism type annotation 2023-10-27 12:25:55 -05:00
Zanie
2a14b94995 Explicitly cast checkout directory to string 2023-10-27 12:23:51 -05:00
Zanie
c4bddb88c9 Use cls in DiagnosticLine.try_from_string 2023-10-27 12:23:04 -05:00
Zanie
8441db69ae Fix total_changes_by_rule type annotation 2023-10-27 12:22:25 -05:00
Zanie
e304d46637 Add mutability note for RuleChanges 2023-10-27 12:21:03 -05:00
Zanie
23050b3653 Guard against dataclass types in jsonable implementation 2023-10-27 12:15:59 -05:00
Zanie
54bc49303f Add deprecation message to old ecosystem script 2023-10-27 12:10:06 -05:00
Zanie
63f1122a5a Run CI workflow when its changed 2023-10-27 12:08:50 -05:00
Zanie
d5127842d7 Undo changes to CI 2023-10-27 12:08:50 -05:00
Zanie Blue
7dc97fd358 Improve readme description.
Co-authored-by: konsti <konstin@mailbox.org>
2023-10-27 12:08:50 -05:00
Zanie
3936bc119a Drop coloring 2023-10-27 12:08:50 -05:00
Zanie
a1bec32a7b Fix error rendering 2023-10-27 12:08:50 -05:00
Zanie
6f593cac68 in files -> across files 2023-10-27 12:08:50 -05:00
Zanie
823ec4cf88 Strip all newlines from error reports 2023-10-27 12:08:50 -05:00
Zanie
087d9c705e Test adding color to diff; fix newline in error report 2023-10-27 12:08:50 -05:00
Zanie
06f71f5235 Remove comma for consistency 2023-10-27 12:08:50 -05:00
Zanie
2270485c09 Display errors in code fences 2023-10-27 12:08:50 -05:00
Zanie
1bbe88f0a0 Fix project truncation 2023-10-27 12:08:50 -05:00
Zanie
e0def9ef95 Truncate more aggressively 2023-10-27 12:08:50 -05:00
Zanie
85de1e97e2 Add completed project count to summary title 2023-10-27 12:08:50 -05:00
Zanie
9cdf864043 Enable all projects again 2023-10-27 12:08:50 -05:00
Zanie
9a8bd09ae6 Fix table 2023-10-27 12:08:50 -05:00
Zanie
2e36781a8a Add more truncation? 2023-10-27 12:08:50 -05:00
Zanie
615e56906e Fix added / removed counts for format 2023-10-27 12:08:50 -05:00
Zanie
ce290076b8 Restore display of fixes 2023-10-27 12:08:50 -05:00
Zanie
1f6b28c29c Fix table 2023-10-27 12:08:50 -05:00
Zanie
d4e9418825 Fix calculation to ignore fixes 2023-10-27 12:08:50 -05:00
Zanie
89f9803f4a Restructuring parsing 2023-10-27 12:08:50 -05:00
Zanie
250b226f16 Restore changelog 2023-10-27 12:08:50 -05:00
Zanie
25bda80e48 Improve limits per rule code 2023-10-27 12:08:50 -05:00
Zanie
88e3663dbe Fix affected rules total 2023-10-27 12:08:50 -05:00
Zanie
5a055aadb0 Fix titles 2023-10-27 12:08:50 -05:00
Zanie
333aedc903 Refactor fixable detection 2023-10-27 12:08:50 -05:00
Zanie
6025081825 Add messy fixable detection to reduce rule changes 2023-10-27 12:08:44 -05:00
Zanie
c806b4a35a Sort imports 2023-10-27 12:07:51 -05:00
Zanie
1c820b0200 Clean up Python implementation; enable all projects 2023-10-27 12:07:51 -05:00
Zanie
1f4e87f043 Use set instead of sorted in RuleChanges.from_diff 2023-10-27 12:07:51 -05:00
Zanie
db5dfd4eff Loosen linked ranges for format diffs since they are not correct 2023-10-27 12:07:51 -05:00
Zanie
c2cae71247 Improve rule table title 2023-10-27 12:07:51 -05:00
Zanie
11e99655b8 Fix formatting of command 2023-10-27 12:07:51 -05:00
Zanie
8898906ef4 Update options display 2023-10-27 12:07:51 -05:00
Zanie
9c63d80257 Change table title 2023-10-27 12:07:51 -05:00
Zanie
d6e35f72a9 Move rule change table into details block 2023-10-27 12:07:51 -05:00
Zanie
c547ec1417 Combine reports into a single artifact again 2023-10-27 12:07:51 -05:00
Zanie
b049c8651a Revert all changes to pr-comment 2023-10-27 12:07:51 -05:00
Zanie
8a48dfe3e3 Continue the battle for if-comment 2023-10-27 12:07:51 -05:00
Zanie
c437a899f8 Fix paths 2023-10-27 12:07:51 -05:00
Zanie
3fd64dac83 Fix file paths 2023-10-27 12:07:51 -05:00
Zanie
28bfab5eff Fix titles 2023-10-27 12:07:51 -05:00
Zanie
f7790698b7 Tweak pull request comment 2023-10-27 12:07:51 -05:00
Zanie
89501451d9 Remove file links (only do line links) 2023-10-27 12:07:51 -05:00
Zanie
368cb3ee95 Add max lines per project to ruff check 2023-10-27 12:07:51 -05:00
Zanie
cbc24a3de2 Use <pre> for check output 2023-10-27 12:07:51 -05:00
Zanie
fc6a3c1c71 Skip more CI checks to speed things up 2023-10-27 12:07:51 -05:00
Zanie
4288d8a6f4 Skip fuzz and bench as well 2023-10-27 12:07:51 -05:00
Zanie
246830c29d Skip tests for now 2023-10-27 12:07:51 -05:00
Zanie
f72c37ff46 Write results separately then combine 2023-10-27 12:07:51 -05:00
Zanie
071c54e89b Add unidiff dep 2023-10-27 12:07:51 -05:00
Zanie
ff3dc646b0 Check against v0.0.292 for debugging 2023-10-27 12:07:51 -05:00
Zanie
d6aa117949 Lint 2023-10-27 12:07:51 -05:00
Zanie
92f935eb2b Update formatter if 2023-10-27 12:07:51 -05:00
Zanie
ab49eaae61 Add formatter ecosystem checks 2023-10-27 12:07:51 -05:00
Zanie
4b79f57872 Refactor ecosystem checks into module 2023-10-27 12:07:51 -05:00
378 changed files with 7641 additions and 9857 deletions

View File

@@ -30,7 +30,7 @@ jobs:
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v40
- uses: tj-actions/changed-files@v39
id: changed
with:
files_yaml: |
@@ -132,7 +132,7 @@ jobs:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
@@ -215,9 +215,6 @@ jobs:
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check
cat ecosystem-result-check > $GITHUB_STEP_SUMMARY
@@ -230,9 +227,6 @@ jobs:
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format
cat ecosystem-result-format > $GITHUB_STEP_SUMMARY
@@ -355,8 +349,8 @@ jobs:
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.generated.yml
check-formatter-instability-and-black-similarity:
name: "formatter instabilities and black similarity"
check-formatter-ecosystem:
name: "Formatter ecosystem and progress checks"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'

View File

@@ -47,7 +47,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.3.2
uses: cloudflare/wrangler-action@v3.3.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.3.2
uses: cloudflare/wrangler-action@v3.3.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -41,7 +41,6 @@ jobs:
workflow: ci.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/ecosystem
workflow_conclusion: completed
if_no_artifact_found: ignore
- name: Generate Comment

View File

@@ -1,36 +0,0 @@
# Until Dependabot support is released https://github.com/dependabot/dependabot-core/issues/1524
name: Pre-commit update
on:
# every week on monday
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:
permissions:
pull-requests: write
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run autoupdate
run: |
pre-commit autoupdate
- name: Commit and push
run: |
git add ".pre-commit-config.yaml"
git commit -m "Upgrade pre-commit dependencies"
git push origin upgrade/pre-commit
- name: Open pull request
run: |
gh pr create --fill

View File

@@ -114,7 +114,7 @@ such that all crates are contained in a flat `crates` directory.
The vast majority of the code, including all lint rules, lives in the `ruff` crate (located at
`crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you.
At the time of writing, the repository includes the following crates:
At time of writing, the repository includes the following crates:
- `crates/ruff_linter`: library crate containing all lint rules and the core logic for running them.
If you're working on a rule, this is the crate for you.
@@ -877,5 +877,5 @@ By default, `src` is set to the project root. In the above example, we'd want to
`src = ["./src"]` to ensure that we locate `./my_project/src/foo` and thus categorize `import foo`
as first-party in `baz.py`. In practice, for this limited example, setting `src = ["./src"]` is
unnecessary, as all imports within `./my_project/src/foo` would be categorized as first-party via
the same-package heuristic; but if your project contains multiple packages, you'll want to set `src`
the same-package heuristic; but your project contains multiple packages, you'll want to set `src`
explicitly.

64
Cargo.lock generated
View File

@@ -313,9 +313,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.7"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
dependencies = [
"clap_builder",
"clap_derive",
@@ -323,9 +323,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.7"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
dependencies = [
"anstream",
"anstyle",
@@ -376,9 +376,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.4.7"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
dependencies = [
"heck",
"proc-macro2",
@@ -388,9 +388,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.6.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "clearscreen"
@@ -1262,9 +1262,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.149"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libcst"
@@ -1309,9 +1309,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lock_api"
@@ -1801,9 +1801,9 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0774c13ff0b8b7ebb4791c050c497aefcfe3f6a222c0829c7017161ed38391ff"
checksum = "569e259cd132eb8cec5df8b672d187c5260f82ad352156b5da9549d4472e64b0"
dependencies = [
"indexmap",
"pep440_rs",
@@ -1912,15 +1912,6 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@@ -2081,6 +2072,7 @@ dependencies = [
"insta-cmd",
"is-macro",
"itertools 0.11.0",
"itoa",
"log",
"mimalloc",
"notify",
@@ -2559,9 +2551,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
dependencies = [
"bitflags 2.4.0",
"errno",
@@ -2673,9 +2665,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.190"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
@@ -2693,9 +2685,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.190"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
@@ -2892,13 +2884,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.8.1"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.48.0",
]
@@ -3331,9 +3323,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"rand",
@@ -3343,9 +3335,9 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.5.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf"
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -15,7 +15,7 @@ license = "MIT"
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.7", features = ["derive"] }
clap = { version = "4.4.6", features = ["derive"] }
colored = { version = "2.0.0" }
filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
@@ -34,7 +34,7 @@ quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.190", features = ["derive"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.3.0", features = ["inline"] }
@@ -52,7 +52,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-ident = { version = "1.0.12" }
unicode_names2 = { version = "1.2.0" }
unicode-width = { version = "0.1.11" }
uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[profile.release]

View File

@@ -33,7 +33,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
of popular Flake8 plugins, like flake8-bugbear
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editor-integrations/) for
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#pyprojecttoml-discovery)

View File

@@ -44,6 +44,7 @@ glob = { workspace = true }
ignore = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
itoa = { version = "1.0.6" }
log = { workspace = true }
notify = { version = "6.1.1" }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
@@ -67,7 +68,7 @@ assert_cmd = { version = "2.0.8" }
colored = { workspace = true, features = ["no-color"]}
insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { version = "0.4.0" }
tempfile = "3.8.1"
tempfile = "3.6.0"
test-case = { workspace = true }
ureq = { version = "2.8.0", features = [] }

View File

@@ -50,11 +50,7 @@ pub enum Command {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
format: HelpFormat,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
@@ -62,11 +58,7 @@ pub enum Command {
Linter {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
format: HelpFormat,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
@@ -507,7 +499,6 @@ impl CheckCommand {
extend_exclude: self.extend_exclude,
extend_fixable: self.extend_fixable,
extend_ignore: self.extend_ignore,
extend_per_file_ignores: self.extend_per_file_ignores,
extend_select: self.extend_select,
extend_unfixable: self.extend_unfixable,
fixable: self.fixable,
@@ -628,7 +619,6 @@ pub struct CliOverrides {
pub ignore: Option<Vec<RuleSelector>>,
pub line_length: Option<LineLength>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub preview: Option<PreviewMode>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<RuleSelector>>,
@@ -659,12 +649,6 @@ impl ConfigurationTransformer for CliOverrides {
if let Some(extend_exclude) = &self.extend_exclude {
config.extend_exclude.extend(extend_exclude.clone());
}
if let Some(extend_per_file_ignores) = &self.extend_per_file_ignores {
config
.lint
.extend_per_file_ignores
.extend(collect_per_file_ignores(extend_per_file_ignores.clone()));
}
if let Some(fix) = &self.fix {
config.fix = Some(*fix);
}

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::fs::{self, File};
use std::hash::Hasher;
@@ -19,7 +20,7 @@ use serde::{Deserialize, Serialize};
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::{DiagnosticKind, Fix};
use ruff_linter::message::Message;
use ruff_linter::{warn_user, VERSION};
use ruff_linter::warn_user;
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
@@ -101,8 +102,9 @@ impl Cache {
pub(crate) fn open(package_root: PathBuf, settings: &Settings) -> Self {
debug_assert!(package_root.is_absolute(), "package root not canonicalized");
let key = format!("{}", cache_key(&package_root, settings));
let path = PathBuf::from_iter([&settings.cache_dir, Path::new(VERSION), Path::new(&key)]);
let mut buf = itoa::Buffer::new();
let key = Path::new(buf.format(cache_key(&package_root, settings)));
let path = PathBuf::from_iter([&settings.cache_dir, Path::new("content"), key]);
let file = match File::open(&path) {
Ok(file) => file,
@@ -140,7 +142,7 @@ impl Cache {
fn empty(path: PathBuf, package_root: PathBuf) -> Self {
let package = PackageCache {
package_root,
files: FxHashMap::default(),
files: HashMap::new(),
};
Cache::new(path, package)
}
@@ -292,7 +294,7 @@ struct PackageCache {
/// single file "packages", e.g. scripts.
package_root: PathBuf,
/// Mapping of source file path to it's cached data.
files: FxHashMap<RelativePathBuf, FileCache>,
files: HashMap<RelativePathBuf, FileCache>,
}
/// On disk representation of the cache per source file.
@@ -348,16 +350,16 @@ struct FileCacheData {
/// version.
fn cache_key(package_root: &Path, settings: &Settings) -> u64 {
let mut hasher = CacheKeyHasher::new();
env!("CARGO_PKG_VERSION").cache_key(&mut hasher);
package_root.cache_key(&mut hasher);
settings.cache_key(&mut hasher);
hasher.finish()
}
/// Initialize the cache at the specified `Path`.
pub(crate) fn init(path: &Path) -> Result<()> {
// Create the cache directories.
fs::create_dir_all(path.join(VERSION))?;
fs::create_dir_all(path.join("content"))?;
// Add the CACHEDIR.TAG.
if !cachedir::is_tagged(path)? {

View File

@@ -82,7 +82,7 @@ pub(crate) fn check(
let settings = resolver.resolve(path, pyproject_config);
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
if !resolved_file.is_root()
&& match_exclusion(
resolved_file.path(),
resolved_file.file_name(),

View File

@@ -18,19 +18,17 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa,
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? {
return Ok(Diagnostics::default());
}
if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? {
return Ok(Diagnostics::default());
}
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
return Ok(Diagnostics::default());
}
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
return Ok(Diagnostics::default());
}
}
let package_root = filename.and_then(Path::parent).and_then(|path| {

View File

@@ -11,7 +11,6 @@ use itertools::Itertools;
use log::{error, warn};
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rustc_hash::FxHashSet;
use thiserror::Error;
use tracing::debug;
@@ -117,14 +116,14 @@ pub(crate) fn format(
return None;
};
let settings = resolver.resolve(path, &pyproject_config);
let resolved_settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
if !resolved_file.is_root()
&& match_exclusion(
path,
resolved_file.file_name(),
&settings.formatter.exclude,
&resolved_settings.formatter.exclude,
)
{
return None;
@@ -139,7 +138,13 @@ pub(crate) fn format(
Some(
match catch_unwind(|| {
format_path(path, &settings.formatter, source_type, mode, cache)
format_path(
path,
&resolved_settings.formatter,
source_type,
mode,
cache,
)
}) {
Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.path().to_path_buf(),
@@ -690,11 +695,11 @@ pub(super) fn warn_incompatible_formatter_settings(
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
// First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default();
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
let mut incompatible_rules = Vec::new();
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
@@ -708,48 +713,41 @@ pub(super) fn warn_incompatible_formatter_settings(
Rule::MissingTrailingComma,
] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.insert(rule);
incompatible_rules.push(rule);
}
}
}
if !incompatible_rules.is_empty() {
let mut rule_names: Vec<_> = incompatible_rules
.into_iter()
.map(|rule| format!("`{}`", rule.noqa_code()))
.collect();
rule_names.sort();
warn_user_once!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.", rule_names.join(", "));
}
// Next, validate settings-specific incompatibilities.
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab()
{
warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`.");
// Rules asserting for space indentation
if setting.formatter.indent_style.is_tab() {
for rule in [Rule::TabIndentation, Rule::IndentWithSpaces] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.push(rule);
}
}
}
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::IndentWithSpaces)
&& setting.formatter.indent_style.is_tab()
{
warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`.");
// Rules asserting for indent-width=4
if setting.formatter.indent_width.value() != 4 {
for rule in [
Rule::IndentationWithInvalidMultiple,
Rule::IndentationWithInvalidMultipleComment,
] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.push(rule);
}
}
}
// Validate all rules that rely on custom indent widths.
if setting.linter.rules.any_enabled(&[
Rule::IndentationWithInvalidMultiple,
Rule::IndentationWithInvalidMultipleComment,
]) && setting.formatter.indent_width.value() != 4
{
warn_user_once!("The `format.indent-width` option with a value other than 4 is incompatible with `E111` and `E114`. We recommend disabling these rules when using the formatter, which enforces a consistent indentation width. Alternatively, set the `format.indent-width` option to `4`.");
if !incompatible_rules.is_empty() {
let mut rule_names: Vec<_> = incompatible_rules
.into_iter()
.map(|rule| format!("`{}`", rule.noqa_code()))
.collect();
rule_names.sort();
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", rule_names.join(", "));
}
// Validate all rules that rely on quote styles.
// Rules with different quote styles.
if setting
.linter
.rules
@@ -760,10 +758,10 @@ pub(super) fn warn_incompatible_formatter_settings(
setting.formatter.quote_style,
) {
(Quote::Double, QuoteStyle::Single) => {
warn_user_once!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
warn!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
}
(Quote::Single, QuoteStyle::Double) => {
warn_user_once!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
warn!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
}
_ => {}
}
@@ -772,26 +770,25 @@ pub(super) fn warn_incompatible_formatter_settings(
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
{
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
}
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
{
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
}
// Validate all isort settings.
if setting.linter.rules.enabled(Rule::UnsortedImports) {
// The formatter removes empty lines if the value is larger than 2 but always inserts a empty line after imports.
// Two empty lines are okay because `isort` only uses this setting for top-level imports (not in nested blocks).
if !matches!(setting.linter.isort.lines_after_imports, 1 | 2 | -1) {
warn_user_once!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).");
warn!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).");
}
// Values larger than two get reduced to one line by the formatter if the import is in a nested block.
if setting.linter.isort.lines_between_types > 1 {
warn_user_once!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).");
warn!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).");
}
// isort inserts a trailing comma which the formatter preserves, but only if `skip-magic-trailing-comma` isn't false.
@@ -800,11 +797,11 @@ pub(super) fn warn_incompatible_formatter_settings(
&& !setting.linter.isort.force_single_line
{
if setting.linter.isort.force_wrap_aliases {
warn_user_once!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.");
warn!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.");
}
if setting.linter.isort.split_on_trailing_comma {
warn_user_once!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.");
warn!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.");
}
}
}

View File

@@ -31,19 +31,17 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
let mode = FormatMode::from_cli(cli);
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
return Ok(ExitStatus::Success);
}
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
return Ok(ExitStatus::Success);
}
}

View File

@@ -188,8 +188,13 @@ pub(crate) fn lint_path(
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
// side-effects that aren't captured in the cache. (In practice, it's fine
// to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll
// write the fixes to disk, thus invalidating the cache. But it's a bit hard
// to reason about. We need to come up with a better solution here.)
let caching = match cache {
Some(cache) if noqa.into() => {
Some(cache) if noqa.into() && fix_mode.is_generate() => {
let relative_path = cache
.relative_path(path)
.expect("wrong package cache for file");
@@ -199,17 +204,7 @@ pub(crate) fn lint_path(
.get(relative_path, &cache_key)
.and_then(|entry| entry.to_diagnostics(path));
if let Some(diagnostics) = cached_diagnostics {
// `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
// and writing the diff to stdout, respectively). If a file has diagnostics, we
// need to avoid reading from and writing to the cache in these modes.
if match fix_mode {
flags::FixMode::Generate => true,
flags::FixMode::Apply | flags::FixMode::Diff => {
diagnostics.messages.is_empty() && diagnostics.fixed.is_empty()
}
} {
return Ok(diagnostics);
}
return Ok(diagnostics);
}
// Stash the file metadata for later so when we update the cache it reflects the prerun
@@ -309,25 +304,15 @@ pub(crate) fn lint_path(
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
if parse_error.is_none() {
// `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
// and writing the diff to stdout, respectively). If a file has diagnostics, we
// need to avoid reading from and writing to the cache in these modes.
if match fix_mode {
flags::FixMode::Generate => true,
flags::FixMode::Apply | flags::FixMode::Diff => {
messages.is_empty() && fixed.is_empty()
}
} {
cache.update_lint(
relative_path.to_owned(),
&key,
LintCacheData::from_messages(
&messages,
imports.clone(),
source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
),
);
}
cache.update_lint(
relative_path.to_owned(),
&key,
LintCacheData::from_messages(
&messages,
imports.clone(),
source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
),
);
}
}

View File

@@ -18,7 +18,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
use crate::args::{Args, CheckCommand, Command, FormatCommand, HelpFormat};
use crate::args::{Args, CheckCommand, Command, FormatCommand};
use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args;
@@ -101,15 +101,6 @@ fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
file == Path::new("-")
}
/// Get the actual value of the `format` desired from either `output_format`
/// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if format.is_some() {
warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
}
format.unwrap_or(output_format)
}
pub fn run(
Args {
command,
@@ -150,18 +141,12 @@ pub fn run(
commands::version::version(output_format)?;
Ok(ExitStatus::Success)
}
Command::Rule {
rule,
all,
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
Command::Rule { rule, all, format } => {
if all {
commands::rule::rules(output_format)?;
commands::rule::rules(format)?;
}
if let Some(rule) = rule {
commands::rule::rule(rule, output_format)?;
commands::rule::rule(rule, format)?;
}
Ok(ExitStatus::Success)
}
@@ -169,12 +154,8 @@ pub fn run(
commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success)
}
Command::Linter {
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
commands::linter::linter(output_format)?;
Command::Linter { format } => {
commands::linter::linter(format)?;
Ok(ExitStatus::Success)
}
Command::Clean => {

View File

@@ -188,73 +188,6 @@ OTHER = "OTHER"
Ok(())
}
#[test]
fn force_exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-exclude = ["out"]
[format]
exclude = ["test.py", "generated.py"]
"#,
)?;
fs::write(
tempdir.path().join("main.py"),
r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#,
)?;
// Excluded file but passed to the CLI directly, should be formatted
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
fs::write(
tempdir.path().join("generated.py"),
r#"NUMBERS = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
]
OTHER = "OTHER"
"#,
)?;
let out_dir = tempdir.path().join("out");
fs::create_dir(&out_dir)?;
fs::write(out_dir.join("a.py"), "a = a")?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--no-cache", "--force-exclude", "--check", "--config"])
.arg(ruff_toml.file_name().unwrap())
// Explicitly pass test.py, should be respect the `format.exclude` when `--force-exclude` is present
.arg(test_path.file_name().unwrap())
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
1 file would be reformatted
----- stderr -----
"###);
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -276,43 +209,6 @@ exclude = ["generated.py"]
.pass_stdin(r#"
from test import say_hy
if __name__ == '__main__':
say_hy("dear Ruff contributor")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
----- stderr -----
"###);
Ok(())
}
#[test]
fn force_exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
ignore = ["Q000", "Q001", "Q002", "Q003"]
[format]
exclude = ["generated.py"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "--force-exclude", "-"])
.pass_stdin(r#"
from test import say_hy
if __name__ == '__main__':
say_hy("dear Ruff contributor")
"#), @r###"
@@ -379,7 +275,7 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
"###);
Ok(())
}
@@ -507,9 +403,7 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
@@ -566,9 +460,7 @@ def say_hy(name: str):
print(f"Hy {name}")
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
@@ -664,7 +556,7 @@ def say_hy(name: str):
----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
"###);
Ok(())
}

View File

@@ -262,13 +262,9 @@ from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#), @r###"
success: false
exit_code: 1
success: true
exit_code: 0
----- stdout -----
generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred
generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -312,87 +308,3 @@ _ = "---------------------------------------------------------------------------
"###);
Ok(())
}
#[test]
fn per_file_ignores_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
.args(["--per-file-ignores", "generated.py:Q"])
.arg("-")
.pass_stdin(r#"
import os
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#), @r###"
success: false
exit_code: 1
----- stdout -----
generated.py:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn extend_per_file_ignores_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
.args(["--extend-per-file-ignores", "generated.py:Q"])
.arg("-")
.pass_stdin(r#"
import os
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#), @r###"
success: false
exit_code: 1
----- stdout -----
generated.py:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}

View File

@@ -41,7 +41,7 @@ serde_json = { workspace = true }
similar = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
tempfile = "3.8.1"
tempfile = "3.6.0"
toml = { workspace = true, features = ["parse"] }
tracing = { workspace = true }
tracing-indicatif = { workspace = true }

View File

@@ -128,11 +128,7 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if let Some(set_name) = parent_set.name() {
if set_name == "format" {
String::from(".format")
} else {
format!(".lint.{set_name}")
}
format!(".{set_name}")
} else {
String::new()
},

View File

@@ -64,7 +64,7 @@ pub(crate) fn generate() -> String {
table_out.push('\n');
table_out.push_str(&format!(
"The {PREVIEW_SYMBOL} emoji indicates that a rule is in [\"preview\"](faq.md#what-is-preview)."
"The {PREVIEW_SYMBOL} emoji indicates that a rule in [\"preview\"](faq.md#what-is-preview)."
));
table_out.push('\n');
table_out.push('\n');

View File

@@ -53,7 +53,7 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pyproject-toml = { version = "0.8.0" }
pyproject-toml = { version = "0.7.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
@@ -79,7 +79,7 @@ pretty_assertions = "1.3.0"
test-case = { workspace = true }
# Disable colored output in tests
colored = { workspace = true, features = ["no-color"] }
tempfile = "3.8.1"
tempfile = "3.6.0"
[features]
default = []

View File

@@ -1,7 +1,7 @@
[tool.ruff]
line-length = 88
[tool.ruff.lint.isort]
[tool.ruff.isort]
lines-after-imports = 3
lines-between-types = 2
known-local-folder = ["ruff"]

View File

@@ -12,19 +12,3 @@ dict['key'] = list[index]
# This is not prohibited by PEP8, but avoid it.
class Foo (Bar, Baz):
pass
def fetch_name () -> Union[str, None]:
"""Fetch name from --person-name in sys.argv.
Returns:
name of the person if available, otherwise None
"""
test = len(5)
Logger.info(test)
# test commented code
# Logger.info("test code")
for i in range (0, len (sys.argv)) :
if sys.argv[i] == "--name" :
return sys.argv[i + 1]
return None

View File

@@ -40,11 +40,5 @@ f"{(a:=1)}"
f"{(lambda x:x)}"
f"normal{f"{a:.3f}"}normal"
#: Okay
snapshot.file_uri[len(f's3://{self.s3_bucket_name}/'):]
#: E231
{len(f's3://{self.s3_bucket_name}/'):1}
#: Okay
a = (1,

View File

@@ -70,10 +70,6 @@ class Apples:
def __html__(self):
pass
# Allow Python's __index__
def __index__(self):
pass
# Allow _missing_, used by enum.Enum.
@classmethod
def _missing_(cls, value):

View File

@@ -1,34 +0,0 @@
import pathlib
NAME = "foo.bar"
open(NAME, "wb")
open(NAME, "w", encoding="utf-8")
open(NAME, "rb")
open(NAME, "x", encoding="utf-8")
open(NAME, "br")
open(NAME, "+r", encoding="utf-8")
open(NAME, "xb")
open(NAME, "rwx") # [bad-open-mode]
open(NAME, mode="rwx") # [bad-open-mode]
open(NAME, "rwx", encoding="utf-8") # [bad-open-mode]
open(NAME, "rr", encoding="utf-8") # [bad-open-mode]
open(NAME, "+", encoding="utf-8") # [bad-open-mode]
open(NAME, "xw", encoding="utf-8") # [bad-open-mode]
open(NAME, "ab+")
open(NAME, "a+b")
open(NAME, "+ab")
open(NAME, "+rUb")
open(NAME, "x+b")
open(NAME, "Ua", encoding="utf-8") # [bad-open-mode]
open(NAME, "Ur++", encoding="utf-8") # [bad-open-mode]
open(NAME, "Ut", encoding="utf-8")
open(NAME, "Ubr")
mode = "rw"
open(NAME, mode)
pathlib.Path(NAME).open("wb")
pathlib.Path(NAME).open(mode)
pathlib.Path(NAME).open("rwx") # [bad-open-mode]
pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
pathlib.Path(NAME).open("rwx", encoding="utf-8") # [bad-open-mode]

View File

@@ -1,22 +0,0 @@
from typing import TYPE_CHECKING
# Verify that statements nested in conditionals (such as top-level type-checking blocks)
# are still considered top-level
if TYPE_CHECKING:
import string
def import_in_function():
import symtable # [import-outside-toplevel]
import os, sys # [import-outside-toplevel]
import time as thyme # [import-outside-toplevel]
import random as rand, socket as sock # [import-outside-toplevel]
from collections import defaultdict # [import-outside-toplevel]
from math import sin as sign, cos as cosplay # [import-outside-toplevel]
class ClassWithImports:
import tokenize # [import-outside-toplevel]
def __init__(self):
import trace # [import-outside-toplevel]

View File

@@ -1,56 +0,0 @@
import threading
from threading import Lock, RLock, Condition, Semaphore, BoundedSemaphore
with threading.Lock(): # [useless-with-lock]
...
with Lock(): # [useless-with-lock]
...
with threading.Lock() as this_shouldnt_matter: # [useless-with-lock]
...
with threading.RLock(): # [useless-with-lock]
...
with RLock(): # [useless-with-lock]
...
with threading.Condition(): # [useless-with-lock]
...
with Condition(): # [useless-with-lock]
...
with threading.Semaphore(): # [useless-with-lock]
...
with Semaphore(): # [useless-with-lock]
...
with threading.BoundedSemaphore(): # [useless-with-lock]
...
with BoundedSemaphore(): # [useless-with-lock]
...
lock = threading.Lock()
with lock: # this is ok
...
rlock = threading.RLock()
with rlock: # this is ok
...
cond = threading.Condition()
with cond: # this is ok
...
sem = threading.Semaphore()
with sem: # this is ok
...
b_sem = threading.BoundedSemaphore()
with b_sem: # this is ok
...

View File

@@ -5,6 +5,4 @@ extend-exclude = [
"migrations",
"with_excluded_file/other_excluded_file.py",
]
[tool.ruff.lint]
per-file-ignores = { "__init__.py" = ["F401"] }

View File

@@ -1,51 +0,0 @@
foo: object
# Errors.
if isinstance(foo, type(None)):
pass
if isinstance(foo, (type(None))):
pass
if isinstance(foo, (type(None), type(None), type(None))):
pass
if isinstance(foo, None | None):
pass
if isinstance(foo, (None | None)):
pass
if isinstance(foo, None | type(None)):
pass
if isinstance(foo, (None | type(None))):
pass
# A bit contrived, but is both technically valid and equivalent to the above.
if isinstance(foo, (type(None) | ((((type(None))))) | ((None | type(None))))):
pass
# Okay.
if isinstance(foo, int):
pass
if isinstance(foo, (int)):
pass
if isinstance(foo, (int, str)):
pass
if isinstance(foo, (int, type(None), str)):
pass
# This is a TypeError, which the rule ignores.
if isinstance(foo, None):
pass
# This is also a TypeError, which the rule ignores.
if isinstance(foo, (None,)):
pass

View File

@@ -1,7 +1,5 @@
[tool.ruff]
src = [".", "python_modules/*"]
exclude = ["examples/excluded"]
[tool.ruff.lint]
extend-select = ["I001"]
extend-ignore = ["F841"]

View File

@@ -170,7 +170,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
expr.start(),
));
if expr.implicit_concatenated {
if pydocstyle::helpers::should_ignore_docstring(expr) {
#[allow(deprecated)]
let location = checker.locator.compute_source_location(expr.start());
warn_user!(

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Operator};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, ExprContext, Operator};
use ruff_python_literal::cformat::{CFormatError, CFormatErrorType};
use ruff_diagnostics::Diagnostic;
@@ -363,18 +363,20 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
let attr = attr.as_str();
if let Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) =
value.as_ref()
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(val),
..
}) = value.as_ref()
{
if attr == "join" {
// "...".join(...) call
if checker.enabled(Rule::StaticJoinToFString) {
flynt::rules::static_join_to_fstring(checker, expr, string);
flynt::rules::static_join_to_fstring(checker, expr, val);
}
} else if attr == "format" {
// "...".format(...) call
let location = expr.range();
match pyflakes::format::FormatSummary::try_from(string.as_ref()) {
match pyflakes::format::FormatSummary::try_from(val.as_ref()) {
Err(e) => {
if checker.enabled(Rule::StringDotFormatInvalidFormat) {
checker.diagnostics.push(Diagnostic::new(
@@ -419,7 +421,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker, string, location,
checker, val, location,
);
}
}
@@ -747,9 +749,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SysExitAlias) {
pylint::rules::sys_exit_alias(checker, func);
}
if checker.enabled(Rule::BadOpenMode) {
pylint::rules::bad_open_mode(checker, call);
}
if checker.enabled(Rule::BadStrStripCall) {
pylint::rules::bad_str_strip_call(checker, func, args);
}
@@ -909,9 +908,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::ExceptionWithoutExcInfo) {
flake8_logging::rules::exception_without_exc_info(checker, call);
}
if checker.enabled(Rule::IsinstanceTypeNone) {
refurb::rules::isinstance_type_none(checker, call);
}
if checker.enabled(Rule::ImplicitCwd) {
refurb::rules::no_implicit_cwd(checker, call);
}
@@ -991,7 +987,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
right,
range: _,
}) => {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = left.as_ref() {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = left.as_ref()
{
if checker.any_enabled(&[
Rule::PercentFormatInvalidFormat,
Rule::PercentFormatExpectedMapping,
@@ -1228,29 +1228,38 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
}
}
Expr::NumberLiteral(_) => {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
}
}
Expr::BytesLiteral(_) => {
Expr::Constant(ast::ExprConstant {
value: Constant::Bytes(_),
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
Expr::StringLiteral(string) => {
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
range: _,
}) => {
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
if let Some(diagnostic) =
flake8_bandit::rules::hardcoded_bind_all_interfaces(string)
flake8_bandit::rules::hardcoded_bind_all_interfaces(value, expr.range())
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, string);
pyupgrade::rules::unicode_kind_prefix(checker, expr, value.unicode);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {

View File

@@ -530,9 +530,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::ImportOutsideTopLevel) {
pylint::rules::import_outside_top_level(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
if let Some(asname) = name.asname.as_ref() {
@@ -709,9 +706,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::ImportOutsideTopLevel) {
pylint::rules::import_outside_top_level(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
if let Some(asname) = name.asname.as_ref() {
@@ -1192,9 +1186,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ReadWholeFile) {
refurb::rules::read_whole_file(checker, with_stmt);
}
if checker.enabled(Rule::UselessWithLock) {
pylint::rules::useless_with_lock(checker, with_stmt);
}
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if checker.enabled(Rule::FunctionUsesLoopVariable) {

View File

@@ -31,8 +31,9 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt,
Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -786,7 +787,11 @@ where
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
{
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
}) = expr
{
self.deferred.string_type_definitions.push((
expr.range(),
value,
@@ -1181,7 +1186,10 @@ where
}
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
range: _,
}) => {
if self.semantic.in_type_definition()
&& !self.semantic.in_literal()
&& !self.semantic.in_f_string()

View File

@@ -211,7 +211,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
#[allow(deprecated)]
@@ -271,14 +270,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement),
(Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException),
(Pylint, "W1501") => (RuleGroup::Preview, rules::pylint::rules::BadOpenMode),
(Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Stable, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Stable, rules::pylint::rules::SubprocessRunWithoutCheck),
(Pylint, "W1514") => (RuleGroup::Preview, rules::pylint::rules::UnspecifiedEncoding),
#[allow(deprecated)]
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2101") => (RuleGroup::Preview, rules::pylint::rules::UselessWithLock),
(Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods),
(Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName),
#[allow(deprecated)]
@@ -938,7 +935,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),

View File

@@ -3,7 +3,7 @@
use std::iter::FusedIterator;
use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_python_ast::{self as ast, Constant, Expr, Stmt, Suite};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{Ranged, TextSize};
@@ -69,15 +69,15 @@ struct StringLinesVisitor<'a> {
impl StatementVisitor<'_> for StringLinesVisitor<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) {
if let Stmt::Expr(ast::StmtExpr {
value: expr,
range: _,
}) = stmt
{
if expr.is_string_literal_expr() {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(..),
..
}) = value.as_ref()
{
for line in UniversalNewlineIterator::with_offset(
self.locator.slice(expr.as_ref()),
expr.start(),
self.locator.slice(value.as_ref()),
value.start(),
) {
self.string_lines.push(line.start());
}

View File

@@ -1,23 +1,30 @@
//! Extract docstrings from an AST.
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind};
/// Extract a docstring from a function or class body.
pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&ast::ExprStringLiteral> {
pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
let stmt = suite.first()?;
// Require the docstring to be a standalone expression.
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return None;
};
// Only match strings.
value.as_string_literal_expr()
if !matches!(
value.as_ref(),
Expr::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
})
) {
return None;
}
Some(value)
}
/// Extract a docstring from a `Definition`.
pub(crate) fn extract_docstring<'a>(
definition: &'a Definition<'a>,
) -> Option<&'a ast::ExprStringLiteral> {
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
match definition {
Definition::Module(module) => docstring_from(module.python_ast),
Definition::Member(member) => docstring_from(member.body()),

View File

@@ -1,7 +1,7 @@
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::ExprStringLiteral;
use ruff_python_ast::Expr;
use ruff_python_semantic::Definition;
use ruff_text_size::{Ranged, TextRange};
@@ -14,8 +14,7 @@ pub(crate) mod styles;
#[derive(Debug)]
pub(crate) struct Docstring<'a> {
pub(crate) definition: &'a Definition<'a>,
/// The literal AST node representing the docstring.
pub(crate) expr: &'a ExprStringLiteral,
pub(crate) expr: &'a Expr,
/// The content of the docstring, including the leading and trailing quotes.
pub(crate) contents: &'a str,
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].

View File

@@ -1,7 +1,8 @@
use colored::Colorize;
use log::warn;
use pyproject_toml::PyProjectToml;
use pyproject_toml::{BuildSystem, Project};
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Serialize};
use ruff_diagnostics::Diagnostic;
use ruff_source_file::SourceFile;
@@ -12,6 +13,16 @@ use crate::rules::ruff::rules::InvalidPyprojectToml;
use crate::settings::LinterSettings;
use crate::IOError;
/// Unlike [`pyproject_toml::PyProjectToml`], in our case `build_system` is also optional
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
/// Build-related data
build_system: Option<BuildSystem>,
/// Project metadata
project: Option<Project>,
}
pub fn lint_pyproject_toml(source_file: SourceFile, settings: &LinterSettings) -> Vec<Message> {
let Some(err) = toml::from_str::<PyProjectToml>(source_file.source_text()).err() else {
return Vec::default();

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::Constant;
use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
@@ -78,7 +79,13 @@ pub(crate) fn variable_name_task_id(
let keyword = arguments.find_keyword("task_id")?;
// If the keyword argument is not a string, we can't do anything.
let ast::ExprStringLiteral { value: task_id, .. } = keyword.value.as_string_literal_expr()?;
let task_id = match &keyword.value {
Expr::Constant(constant) => match &constant.value {
Constant::Str(ast::StringConstant { value, .. }) => value,
_ => return None,
},
_ => return None,
};
// If the target name is the same as the task_id, no violation.
if id == task_id {

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -230,16 +230,16 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
Expr::Subscript(ast::ExprSubscript { value, slice, .. })
if is_sys(value, "version_info", checker.semantic()) =>
{
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(i),
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) = slice.as_ref()
{
if *i == 0 {
if let (
[CmpOp::Eq | CmpOp::NotEq],
[Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(n),
[Expr::Constant(ast::ExprConstant {
value: Constant::Int(n),
..
})],
) = (ops, comparators)
@@ -253,8 +253,8 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
} else if *i == 1 {
if let (
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
[Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
[Expr::Constant(ast::ExprConstant {
value: Constant::Int(_),
..
})],
) = (ops, comparators)
@@ -274,8 +274,8 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
{
if let (
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
[Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
[Expr::Constant(ast::ExprConstant {
value: Constant::Int(_),
..
})],
) = (ops, comparators)
@@ -294,10 +294,13 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
if is_sys(left, "version", checker.semantic()) {
if let (
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
[Expr::StringLiteral(ast::ExprStringLiteral { value, .. })],
[Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
..
})],
) = (ops, comparators)
{
if value.len() == 1 {
if s.len() == 1 {
if checker.enabled(Rule::SysVersionCmpStr10) {
checker
.diagnostics

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -177,8 +177,8 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
step: None,
range: _,
}) => {
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(i),
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) = upper.as_ref()
{
@@ -194,8 +194,8 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
}
}
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(i),
Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) => {
if *i == 2 && checker.enabled(Rule::SysVersion2) {

View File

@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault, Stmt};
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Stmt};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::Definition;
@@ -265,7 +265,7 @@ impl Violation for MissingReturnTypePrivateFunction {
/// or `ruff.toml` file:
///
/// ```toml
/// [tool.ruff.lint.flake8-annotations]
/// [tool.ruff.flake8-annotations]
/// mypy-init-return = true
/// ```
///
@@ -431,7 +431,10 @@ fn is_none_returning(body: &[Stmt]) -> bool {
visitor.visit_body(body);
for stmt in visitor.returns {
if let Some(value) = stmt.value.as_deref() {
if !value.is_none_literal_expr() {
if !matches!(
value,
Expr::Constant(constant) if constant.value.is_none()
) {
return false;
}
}
@@ -448,10 +451,9 @@ fn check_dynamically_typed<F>(
) where
F: FnOnce() -> String,
{
if let Expr::StringLiteral(ast::ExprStringLiteral {
if let Expr::Constant(ast::ExprConstant {
range,
value: string,
..
value: Constant::Str(string),
}) = annotation
{
// Quoted annotations

View File

@@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_semantic::SemanticModel;
@@ -10,7 +10,10 @@ static PASSWORD_CANDIDATE_REGEX: Lazy<Regex> = Lazy::new(|| {
pub(super) fn string_literal(expr: &Expr) -> Option<&str> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value),
Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
..
}) => Some(string),
_ => None,
}
}

View File

@@ -3,7 +3,7 @@ use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_ast::{self as ast, Constant, Expr, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -144,8 +144,8 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
/// an integer value, but that value is out of range.
fn parse_mask(expr: &Expr, semantic: &SemanticModel) -> Result<Option<u16>> {
match expr {
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(int),
Expr::Constant(ast::ExprConstant {
value: Constant::Int(int),
..
}) => match int.as_u16() {
Some(value) => Ok(Some(value)),

View File

@@ -1,6 +1,7 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprStringLiteral;
/// ## What it does
/// Checks for hardcoded bindings to all network interfaces (`0.0.0.0`).
@@ -34,9 +35,9 @@ impl Violation for HardcodedBindAllInterfaces {
}
/// S104
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
if string.value == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
pub(crate) fn hardcoded_bind_all_interfaces(value: &str, range: TextRange) -> Option<Diagnostic> {
if value == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, range))
} else {
None
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -55,7 +55,10 @@ fn password_target(target: &Expr) -> Option<&str> {
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
// d["password"] = "s3cr3t"
Expr::Subscript(ast::ExprSubscript { slice, .. }) => match slice.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value,
Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
..
}) => string,
_ => return None,
},
// obj.password = "s3cr3t"

View File

@@ -2,6 +2,7 @@ use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -51,13 +52,13 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprStringLiteral) {
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, expr: &Expr, value: &str) {
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| string.value.starts_with(prefix))
.any(|prefix| value.starts_with(prefix))
{
return;
}
@@ -76,8 +77,8 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: string.value.clone(),
string: value.to_string(),
},
string.range,
expr.range(),
));
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -66,7 +66,10 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, call: &ast::ExprCal
{
if let Some(keyword) = call.arguments.find_keyword("autoescape") {
match &keyword.value {
Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: true, .. }) => (),
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),
..
}) => (),
Expr::Call(ast::ExprCall { func, .. }) => {
if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() {
if id != "select_autoescape" {

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::is_const_none;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -64,7 +64,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, call: &ast::ExprCal
})
{
if let Some(keyword) = call.arguments.find_keyword("timeout") {
if keyword.value.is_none_literal_expr() {
if is_const_none(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
keyword.range(),

View File

@@ -3,7 +3,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -38,22 +38,18 @@ use crate::{
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
#[violation]
pub struct SubprocessPopenWithShellEqualsTrue {
safety: Safety,
is_exact: bool,
seems_safe: bool,
}
impl Violation for SubprocessPopenWithShellEqualsTrue {
#[derive_message_formats]
fn message(&self) -> String {
match (self.safety, self.is_exact) {
(Safety::SeemsSafe, true) => format!(
if self.seems_safe {
format!(
"`subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`"
),
(Safety::Unknown, true) => format!("`subprocess` call with `shell=True` identified, security issue"),
(Safety::SeemsSafe, false) => format!(
"`subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`"
),
(Safety::Unknown, false) => format!("`subprocess` call with truthy `shell` identified, security issue"),
)
} else {
format!("`subprocess` call with `shell=True` identified, security issue")
}
}
}
@@ -93,18 +89,16 @@ impl Violation for SubprocessWithoutShellEqualsTrue {
}
/// ## What it does
/// Checks for method calls that set the `shell` parameter to `true` or another
/// truthy value when invoking a subprocess.
/// Checks for method calls that set the `shell` parameter to `true` when
/// invoking a subprocess.
///
/// ## Why is this bad?
/// Setting the `shell` parameter to `true` or another truthy value when
/// invoking a subprocess can introduce security vulnerabilities, as it allows
/// shell metacharacters and whitespace to be passed to child processes,
/// potentially leading to shell injection attacks.
///
/// It is recommended to avoid using `shell=True` unless absolutely necessary
/// and, when used, to ensure that all inputs are properly sanitized and quoted
/// to prevent such vulnerabilities.
/// Setting the `shell` parameter to `true` when invoking a subprocess can
/// introduce security vulnerabilities, as it allows shell metacharacters and
/// whitespace to be passed to child processes, potentially leading to shell
/// injection attacks. It is recommended to avoid using `shell=True` unless
/// absolutely necessary, and when used, to ensure that all inputs are properly
/// sanitized and quoted to prevent such vulnerabilities.
///
/// ## Known problems
/// Prone to false positives as it is triggered on any function call with a
@@ -121,18 +115,12 @@ impl Violation for SubprocessWithoutShellEqualsTrue {
/// ## References
/// - [Python documentation: Security Considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)
#[violation]
pub struct CallWithShellEqualsTrue {
is_exact: bool,
}
pub struct CallWithShellEqualsTrue;
impl Violation for CallWithShellEqualsTrue {
#[derive_message_formats]
fn message(&self) -> String {
if self.is_exact {
format!("Function call with `shell=True` parameter identified, security issue")
} else {
format!("Function call with truthy `shell` parameter identified, security issue")
}
format!("Function call with `shell=True` parameter identified, security issue")
}
}
@@ -174,15 +162,16 @@ impl Violation for CallWithShellEqualsTrue {
/// - [Python documentation: `subprocess`](https://docs.python.org/3/library/subprocess.html)
#[violation]
pub struct StartProcessWithAShell {
safety: Safety,
seems_safe: bool,
}
impl Violation for StartProcessWithAShell {
#[derive_message_formats]
fn message(&self) -> String {
match self.safety {
Safety::SeemsSafe => format!("Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`"),
Safety::Unknown => format!("Starting a process with a shell, possible injection detected"),
if self.seems_safe {
format!("Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`")
} else {
format!("Starting a process with a shell, possible injection detected")
}
}
}
@@ -295,14 +284,13 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
match shell_keyword {
// S602
Some(ShellKeyword {
truthiness: truthiness @ (Truthiness::True | Truthiness::Truthy),
truthiness: Truthiness::Truthy,
keyword,
}) => {
if checker.enabled(Rule::SubprocessPopenWithShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(
SubprocessPopenWithShellEqualsTrue {
safety: Safety::from(arg),
is_exact: matches!(truthiness, Truthiness::True),
seems_safe: shell_call_seems_safe(arg),
},
keyword.range(),
));
@@ -310,7 +298,7 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
}
// S603
Some(ShellKeyword {
truthiness: Truthiness::False | Truthiness::Falsey | Truthiness::Unknown,
truthiness: Truthiness::Falsey | Truthiness::Unknown,
keyword,
}) => {
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
@@ -332,18 +320,15 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
}
}
} else if let Some(ShellKeyword {
truthiness: truthiness @ (Truthiness::True | Truthiness::Truthy),
truthiness: Truthiness::Truthy,
keyword,
}) = shell_keyword
{
// S604
if checker.enabled(Rule::CallWithShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(
CallWithShellEqualsTrue {
is_exact: matches!(truthiness, Truthiness::True),
},
keyword.range(),
));
checker
.diagnostics
.push(Diagnostic::new(CallWithShellEqualsTrue, keyword.range()));
}
}
@@ -353,7 +338,7 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
if let Some(arg) = call.arguments.args.first() {
checker.diagnostics.push(Diagnostic::new(
StartProcessWithAShell {
safety: Safety::from(arg),
seems_safe: shell_call_seems_safe(arg),
},
arg.range(),
));
@@ -391,7 +376,7 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
(
Some(CallKind::Subprocess),
Some(ShellKeyword {
truthiness: Truthiness::True | Truthiness::Truthy,
truthiness: Truthiness::Truthy,
keyword: _,
})
)
@@ -468,22 +453,16 @@ fn find_shell_keyword<'a>(
})
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Safety {
SeemsSafe,
Unknown,
}
impl From<&Expr> for Safety {
/// Return the [`Safety`] level for the [`Expr`]. This is based on Bandit's definition: string
/// literals are considered okay, but dynamically-computed values are not.
fn from(expr: &Expr) -> Self {
if expr.is_string_literal_expr() {
Self::SeemsSafe
} else {
Self::Unknown
}
}
/// Return `true` if the value provided to the `shell` call seems safe. This is based on Bandit's
/// definition: string literals are considered okay, but dynamically-computed values are not.
fn shell_call_seems_safe(arg: &Expr) -> bool {
matches!(
arg,
Expr::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
})
)
}
/// Return `true` if the string appears to be a full file path.

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Int};
use ruff_python_ast::{self as ast, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -52,8 +52,8 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall)
if let Some(keyword) = call.arguments.find_keyword("mpModel") {
if matches!(
keyword.value,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(Int::ZERO | Int::ONE),
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ZERO | Int::ONE),
..
})
) {

View File

@@ -854,7 +854,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
// If the `url` argument is a string literal, allow `http` and `https` schemes.
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: url, .. })) = &call.arguments.find_argument("url", 0) {
if let Some(Expr::Constant(ast::ExprConstant { value: ast::Constant::Str(url), .. })) = &call.arguments.find_argument("url", 0) {
let url = url.trim_start();
if url.starts_with("http://") || url.starts_with("https://") {
return None;

View File

@@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall};
use ruff_python_ast::{self as ast, Constant, Expr, ExprAttribute, ExprCall};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -151,8 +151,8 @@ fn extract_cryptographic_key(
fn extract_int_argument(call: &ExprCall, name: &str, position: usize) -> Option<(u16, TextRange)> {
let argument = call.arguments.find_argument(name, position)?;
let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(i),
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) = argument
else {

View File

@@ -49,7 +49,7 @@ S602.py:8:13: S602 `subprocess` call with `shell=True` seems safe, but may be ch
10 | # Check values that truthy values are treated as true.
|
S602.py:11:15: S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
S602.py:11:15: S602 `subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`
|
10 | # Check values that truthy values are treated as true.
11 | Popen("true", shell=1)
@@ -58,7 +58,7 @@ S602.py:11:15: S602 `subprocess` call with truthy `shell` seems safe, but may be
13 | Popen("true", shell={1: 1})
|
S602.py:12:15: S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
S602.py:12:15: S602 `subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`
|
10 | # Check values that truthy values are treated as true.
11 | Popen("true", shell=1)
@@ -68,7 +68,7 @@ S602.py:12:15: S602 `subprocess` call with truthy `shell` seems safe, but may be
14 | Popen("true", shell=(1,))
|
S602.py:13:15: S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
S602.py:13:15: S602 `subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`
|
11 | Popen("true", shell=1)
12 | Popen("true", shell=[1])
@@ -77,7 +77,7 @@ S602.py:13:15: S602 `subprocess` call with truthy `shell` seems safe, but may be
14 | Popen("true", shell=(1,))
|
S602.py:14:15: S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
S602.py:14:15: S602 `subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`
|
12 | Popen("true", shell=[1])
13 | Popen("true", shell={1: 1})

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
@@ -62,3 +62,14 @@ pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
false
}
/// Returns `true` if an expression is a boolean literal.
pub(super) const fn is_boolean(expr: &Expr) -> bool {
matches!(
&expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(_),
..
})
)
}

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::{is_allowed_func_def, is_boolean};
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
@@ -117,10 +117,7 @@ pub(crate) fn boolean_default_value_positional_argument(
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
if default
.as_ref()
.is_some_and(|default| default.is_boolean_literal_expr())
{
if default.as_ref().is_some_and(|default| is_boolean(default)) {
checker.diagnostics.push(Diagnostic::new(
BooleanDefaultValuePositionalArgument,
parameter.name.range(),

View File

@@ -4,7 +4,7 @@ use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap;
use crate::rules::flake8_boolean_trap::helpers::{allow_boolean_trap, is_boolean};
/// ## What it does
/// Checks for boolean positional arguments in function calls.
@@ -49,7 +49,7 @@ pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Ex
if allow_boolean_trap(func) {
return;
}
for arg in args.iter().filter(|arg| arg.is_boolean_literal_expr()) {
for arg in args.iter().filter(|arg| is_boolean(arg)) {
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Decorator, Expr, ParameterWithDefault, Parameters};
use ruff_python_ast::{self as ast, Constant, Decorator, Expr, ParameterWithDefault, Parameters};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
@@ -126,7 +126,10 @@ pub(crate) fn boolean_type_hint_positional_argument(
// check for both bool (python class) and 'bool' (string annotation)
let hint = match annotation.as_ref() {
Expr::Name(name) => &name.id == "bool",
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "bool",
Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) => value == "bool",
_ => false,
};
if !hint || !checker.semantic().is_builtin("bool") {

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Stmt};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -121,12 +121,12 @@ fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel)
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
}
Stmt::Expr(ast::StmtExpr { value, range: _ }) => match value.as_ref() {
Expr::Constant(ast::ExprConstant { value, .. }) => {
matches!(value, Constant::Str(..) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
})
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Expr};
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
@@ -42,13 +42,13 @@ impl Violation for DuplicateValue {
pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec<Expr>) {
let mut seen_values: FxHashSet<ComparableExpr> = FxHashSet::default();
for elt in elts {
if elt.is_literal_expr() {
if let Expr::Constant(ast::ExprConstant { value, .. }) = elt {
let comparable_value: ComparableExpr = elt.into();
if !seen_values.insert(comparable_value) {
checker.diagnostics.push(Diagnostic::new(
DuplicateValue {
value: checker.generator().expr(elt),
value: checker.generator().constant(value),
},
elt.range(),
));

View File

@@ -1,7 +1,7 @@
use crate::fix::edits::pad;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use ruff_text_size::Ranged;
@@ -66,7 +66,11 @@ pub(crate) fn getattr_with_constant(
if obj.is_starred_expr() {
return;
}
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = arg
else {
return;
};
if !is_identifier(value) {

View File

@@ -38,7 +38,7 @@ impl Violation for RaiseLiteral {
/// B016
pub(crate) fn raise_literal(checker: &mut Checker, expr: &Expr) {
if expr.is_literal_expr() {
if expr.is_constant_expr() {
checker
.diagnostics
.push(Diagnostic::new(RaiseLiteral, expr.range()));

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, ExprContext, Identifier, Stmt};
use ruff_python_ast::{self as ast, Constant, Expr, ExprContext, Identifier, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
@@ -80,7 +80,11 @@ pub(crate) fn setattr_with_constant(
if obj.is_starred_expr() {
return;
}
let Expr::StringLiteral(ast::ExprStringLiteral { value: name, .. }) = name else {
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name),
..
}) = name
else {
return;
};
if !is_identifier(name) {

View File

@@ -1,5 +1,5 @@
use itertools::Itertools;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -68,7 +68,11 @@ pub(crate) fn strip_with_multi_characters(
return;
}
let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = args else {
let [Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
})] = args
else {
return;
};

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -67,7 +67,11 @@ pub(crate) fn unreliable_callable_check(
let [obj, attr, ..] = args else {
return;
};
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = attr else {
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = attr
else {
return;
};
if value != "__call__" {

View File

@@ -32,8 +32,8 @@ impl Violation for UselessComparison {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Pointless comparison. Did you mean to assign a value? \
Otherwise, prepend `assert` or remove it."
"Pointless comparison. This comparison does nothing but waste CPU instructions. \
Either prepend `assert` or remove it."
)
}
}

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -54,7 +54,11 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::FString(_) | Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Ellipsis,
..
})
) {
return;
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -81,14 +81,14 @@ fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
if args.len() == 2 && is_const_none(&args[1]) {
return true;
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
if is_const_none(&keyword.value) {
return true;
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B015.py:3:1: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
B015.py:3:1: B015 Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it.
|
1 | assert 1 == 1
2 |
@@ -11,7 +11,7 @@ B015.py:3:1: B015 Pointless comparison. Did you mean to assign a value? Otherwis
5 | assert 1 in (1, 2)
|
B015.py:7:1: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
B015.py:7:1: B015 Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it.
|
5 | assert 1 in (1, 2)
6 |
@@ -19,7 +19,7 @@ B015.py:7:1: B015 Pointless comparison. Did you mean to assign a value? Otherwis
| ^^^^^^^^^^^ B015
|
B015.py:17:5: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
B015.py:17:5: B015 Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it.
|
15 | assert 1 in (1, 2)
16 |
@@ -27,7 +27,7 @@ B015.py:17:5: B015 Pointless comparison. Did you mean to assign a value? Otherwi
| ^^^^^^^^^^^ B015
|
B015.py:24:5: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
B015.py:24:5: B015 Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it.
|
23 | class TestClass:
24 | 1 == 1

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, UnaryOp};
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -79,8 +79,8 @@ pub(crate) fn unnecessary_subscript_reversal(checker: &mut Checker, call: &ast::
else {
return;
};
let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(val),
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(val),
..
}) = operand.as_ref()
else {

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
@@ -86,7 +86,7 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::Exp
}
// none args
if call.arguments.args.len() > 1 && call.arguments.args[1].is_none_literal_expr() {
if call.arguments.args.len() > 1 && is_const_none(&call.arguments.args[1]) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeFromtimestamp, call.range()));

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -77,12 +77,7 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast
}
// none args
if call
.arguments
.args
.first()
.is_some_and(Expr::is_none_literal_expr)
{
if call.arguments.args.first().is_some_and(is_const_none) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, call.range()));

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -75,8 +75,10 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
}
// Does the `strptime` call contain a format string with a timezone specifier?
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: format, .. })) =
call.arguments.args.get(1).as_ref()
if let Some(Expr::Constant(ast::ExprConstant {
value: Constant::Str(format),
range: _,
})) = call.arguments.args.get(1).as_ref()
{
if format.contains("%z") {
return;

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -73,12 +73,7 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::Ex
}
// Positional arg: is constant None.
if call
.arguments
.args
.get(7)
.is_some_and(Expr::is_none_literal_expr)
{
if call.arguments.args.get(7).is_some_and(is_const_none) {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, call.range()));

View File

@@ -1,3 +1,4 @@
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{Arguments, Expr, ExprAttribute};
use crate::checkers::ast::Checker;
@@ -14,5 +15,5 @@ pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
pub(super) fn has_non_none_keyword(arguments: &Arguments, keyword: &str) -> bool {
arguments
.find_keyword(keyword)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
.is_some_and(|keyword| !is_const_none(&keyword.value))
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -80,13 +80,16 @@ pub(crate) fn all_with_model_form(
if id != "fields" {
continue;
}
match value.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() else {
continue;
};
match value {
Constant::Str(ast::StringConstant { value, .. }) => {
if value == "__all__" {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}
}
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
Constant::Bytes(ast::BytesConstant { value, .. }) => {
if value == "__all__".as_bytes() {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Stmt};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, ExprContext, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
@@ -182,7 +182,10 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
if let Some(first) = args.first() {
match first {
// Check for string literals.
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
..
}) => {
if checker.enabled(Rule::RawStringInException) {
if string.len() >= checker.settings.flake8_errmsg.max_string_length {
let mut diagnostic =
@@ -229,7 +232,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) =
func.as_ref()
{
if attr == "format" && value.is_literal_expr() {
if attr == "format" && value.is_constant_expr() {
let mut diagnostic =
Diagnostic::new(DotFormatInException, first.range());
if let Some(indentation) =

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_ast::{self as ast, Constant, Expr, Operator};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
@@ -58,7 +58,11 @@ pub(crate) fn printf_in_gettext_func_call(checker: &mut Checker, args: &[Expr])
..
}) = &first
{
if left.is_string_literal_expr() {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
}) = left.as_ref()
{
checker
.diagnostics
.push(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range()));

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_ast::{self as ast, Constant, Expr, Operator};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -51,10 +51,18 @@ pub(crate) fn explicit(expr: &Expr, locator: &Locator) -> Option<Diagnostic> {
if matches!(op, Operator::Add) {
if matches!(
left.as_ref(),
Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..
})
) && matches!(
right.as_ref(),
Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..
})
) && locator.contains_line_break(*range)
{
return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range()));

View File

@@ -83,7 +83,6 @@ fn exc_info_arg_is_falsey(call: &ExprCall, checker: &mut Checker) -> bool {
.find_keyword("exc_info")
.map(|keyword| &keyword.value)
.is_some_and(|value| {
let truthiness = Truthiness::from_expr(value, |id| checker.semantic().is_builtin(id));
matches!(truthiness, Truthiness::False | Truthiness::Falsey)
Truthiness::from_expr(value, |id| checker.semantic().is_builtin(id)).is_falsey()
})
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Operator};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
@@ -74,7 +74,7 @@ fn check_msg(checker: &mut Checker, msg: &Expr) {
Expr::Call(ast::ExprCall { func, .. }) => {
if checker.enabled(Rule::LoggingStringFormat) {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
if attr == "format" && value.is_literal_expr() {
if attr == "format" && value.is_constant_expr() {
checker
.diagnostics
.push(Diagnostic::new(LoggingStringFormat, msg.range()));
@@ -92,7 +92,11 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
Expr::Dict(ast::ExprDict { keys, .. }) => {
for key in keys {
if let Some(key) = &key {
if let Expr::StringLiteral(ast::ExprStringLiteral { value: attr, .. }) = key {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(attr),
..
}) = key
{
if is_reserved_attr(attr) {
checker.diagnostics.push(Diagnostic::new(
LoggingExtraAttrClash(attr.to_string()),

View File

@@ -1,6 +1,6 @@
use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_python_ast::{self as ast, Expr, Keyword};
use ruff_python_ast::{self as ast, Constant, Expr, Keyword};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
@@ -96,7 +96,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
.iter()
.zip(values.iter())
.map(|(kwarg, value)| {
format!("{}={}", kwarg, checker.locator().slice(value.range()))
format!("{}={}", kwarg.value, checker.locator().slice(value.range()))
})
.join(", "),
kw.range(),
@@ -108,8 +108,12 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
}
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
fn as_kwarg(key: &Expr) -> Option<&str> {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {
fn as_kwarg(key: &Expr) -> Option<&ast::StringConstant> {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
}) = key
{
if is_identifier(value) {
return Some(value);
}

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysFixableViolation, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -65,8 +65,8 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
};
// Verify that the `start` argument is the literal `0`.
let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(value),
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = start
else {

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
@@ -86,7 +86,7 @@ pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
// If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`,
// or `"sys.stderr"`), don't trigger T201.
if let Some(keyword) = call.arguments.find_keyword("file") {
if !keyword.value.is_none_literal_expr() {
if !is_const_none(&keyword.value) {
if checker.semantic().resolve_call_path(&keyword.value).map_or(
true,
|call_path| {

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::ExprStringLiteral;
use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -36,8 +36,8 @@ impl Violation for DocstringInStub {
}
/// PYI021
pub(crate) fn docstring_in_stubs(checker: &mut Checker, docstring: Option<&ExprStringLiteral>) {
if let Some(docstr) = docstring {
pub(crate) fn docstring_in_stubs(checker: &mut Checker, docstring: Option<&Expr>) {
if let Some(docstr) = &docstring {
checker
.diagnostics
.push(Diagnostic::new(DocstringInStub, docstr.range()));

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Stmt, StmtExpr};
use ruff_python_ast::{Constant, Expr, ExprConstant, Stmt, StmtExpr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -54,7 +54,13 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[St
continue;
};
if value.is_ellipsis_literal_expr() {
if matches!(
value.as_ref(),
Expr::Constant(ExprConstant {
value: Constant::Ellipsis,
..
})
) {
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
let edit =
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());

View File

@@ -8,7 +8,7 @@ use smallvec::SmallVec;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -245,7 +245,7 @@ fn non_none_annotation_element<'a>(
// E.g., `typing.Union` or `typing.Optional`
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation {
if semantic.match_typing_expr(value, "Optional") {
return if slice.is_none_literal_expr() {
return if is_const_none(slice) {
None
} else {
Some(slice)
@@ -264,7 +264,7 @@ fn non_none_annotation_element<'a>(
return None;
};
return match (left.is_none_literal_expr(), right.is_none_literal_expr()) {
return match (is_const_none(left), is_const_none(right)) {
(false, true) => Some(left),
(true, false) => Some(right),
(true, true) => None,
@@ -280,11 +280,11 @@ fn non_none_annotation_element<'a>(
..
}) = annotation
{
if !left.is_none_literal_expr() {
if !is_const_none(left) {
return Some(left);
}
if !right.is_none_literal_expr() {
if !is_const_none(right) {
return Some(right);
}

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -60,8 +60,10 @@ pub(crate) fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) {
// Ignore `...` (the desired case).
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if value.is_ellipsis_literal_expr() {
return;
if let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() {
if value.is_ellipsis() {
return;
}
}
}

View File

@@ -1,9 +1,10 @@
use rustc_hash::FxHashSet;
use std::fmt;
use ast::Constant;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -59,7 +60,7 @@ impl Violation for RedundantLiteralUnion {
/// PYI051
pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr) {
let mut typing_literal_exprs = Vec::new();
let mut literal_exprs = Vec::new();
let mut builtin_types_in_union = FxHashSet::default();
// Adds a member to `literal_exprs` for each value in a `Literal`, and any builtin types
@@ -68,9 +69,9 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
typing_literal_exprs.extend(elts.iter());
literal_exprs.extend(elts.iter());
} else {
typing_literal_exprs.push(slice);
literal_exprs.push(slice);
}
}
return;
@@ -84,20 +85,18 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
traverse_union(&mut func, checker.semantic(), union, None);
for typing_literal_expr in typing_literal_exprs {
let Some(literal_type) = match_literal_type(typing_literal_expr) else {
for literal_expr in literal_exprs {
let Some(constant_type) = match_constant_type(literal_expr) else {
continue;
};
if builtin_types_in_union.contains(&literal_type) {
if builtin_types_in_union.contains(&constant_type) {
checker.diagnostics.push(Diagnostic::new(
RedundantLiteralUnion {
literal: SourceCodeSnippet::from_str(
checker.locator().slice(typing_literal_expr),
),
builtin_type: literal_type,
literal: SourceCodeSnippet::from_str(checker.locator().slice(literal_expr)),
builtin_type: constant_type,
},
typing_literal_expr.range(),
literal_expr.range(),
));
}
}
@@ -145,24 +144,18 @@ fn match_builtin_type(expr: &Expr, semantic: &SemanticModel) -> Option<ExprType>
Some(result)
}
/// Return the [`ExprType`] of an [`Expr`] if it is a literal (e.g., an `int`, like `1`, or a
/// Return the [`ExprType`] of an [`Expr]` if it is a constant (e.g., an `int`, like `1`, or a
/// `bool`, like `True`).
fn match_literal_type(expr: &Expr) -> Option<ExprType> {
let Some(literal_expr) = expr.as_literal_expr() else {
return None;
};
let result = match literal_expr {
LiteralExpressionRef::BooleanLiteral(_) => ExprType::Bool,
LiteralExpressionRef::StringLiteral(_) => ExprType::Str,
LiteralExpressionRef::BytesLiteral(_) => ExprType::Bytes,
LiteralExpressionRef::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value {
ast::Number::Int(_) => ExprType::Int,
ast::Number::Float(_) => ExprType::Float,
ast::Number::Complex { .. } => ExprType::Complex,
},
LiteralExpressionRef::NoneLiteral(_) | LiteralExpressionRef::EllipsisLiteral(_) => {
return None;
}
fn match_constant_type(expr: &Expr) -> Option<ExprType> {
let constant = expr.as_constant_expr()?;
let result = match constant.value {
Constant::Bool(_) => ExprType::Bool,
Constant::Str(_) => ExprType::Str,
Constant::Bytes(_) => ExprType::Bytes,
Constant::Int(_) => ExprType::Int,
Constant::Float(_) => ExprType::Float,
Constant::Complex { .. } => ExprType::Complex,
_ => return None,
};
Some(result)
}

View File

@@ -2,7 +2,8 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation}
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::{
self as ast, Arguments, Expr, Operator, ParameterWithDefault, Parameters, Stmt, UnaryOp,
self as ast, Arguments, Constant, Expr, Operator, ParameterWithDefault, Parameters, Stmt,
UnaryOp,
};
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_source_file::Locator;
@@ -281,12 +282,7 @@ fn is_valid_default_value_with_annotation(
semantic: &SemanticModel,
) -> bool {
match default {
Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_)
| Expr::NoneLiteral(_)
| Expr::EllipsisLiteral(_) => {
Expr::Constant(_) => {
return true;
}
Expr::List(ast::ExprList { elts, .. })
@@ -318,7 +314,10 @@ fn is_valid_default_value_with_annotation(
}) => {
match operand.as_ref() {
// Ex) `-1`, `-3.14`, `2j`
Expr::NumberLiteral(_) => return true,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(..) | Constant::Float(..) | Constant::Complex { .. },
..
}) => return true,
// Ex) `-math.inf`, `-math.pi`, etc.
Expr::Attribute(_) => {
if semantic
@@ -339,14 +338,14 @@ fn is_valid_default_value_with_annotation(
range: _,
}) => {
// Ex) `1 + 2j`, `1 - 2j`, `-1 - 2j`, `-1 + 2j`
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Complex { .. },
if let Expr::Constant(ast::ExprConstant {
value: Constant::Complex { .. },
..
}) = right.as_ref()
{
// Ex) `1 + 2j`, `1 - 2j`
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(..) | ast::Number::Float(..),
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(..) | Constant::Float(..),
..
}) = left.as_ref()
{
@@ -358,8 +357,8 @@ fn is_valid_default_value_with_annotation(
}) = left.as_ref()
{
// Ex) `-1 + 2j`, `-1 - 2j`
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(..) | ast::Number::Float(..),
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(..) | Constant::Float(..),
..
}) = operand.as_ref()
{
@@ -394,7 +393,13 @@ fn is_valid_pep_604_union(annotation: &Expr) -> bool {
right,
range: _,
}) => is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right),
Expr::Name(_) | Expr::Subscript(_) | Expr::Attribute(_) | Expr::NoneLiteral(_) => true,
Expr::Name(_)
| Expr::Subscript(_)
| Expr::Attribute(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::None,
..
}) => true,
_ => false,
}
}
@@ -422,8 +427,10 @@ fn is_valid_default_value_without_annotation(default: &Expr) -> bool {
| Expr::Name(_)
| Expr::Attribute(_)
| Expr::Subscript(_)
| Expr::EllipsisLiteral(_)
| Expr::NoneLiteral(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Ellipsis | Constant::None,
..
})
) || is_valid_pep_604_union(default)
}
@@ -492,8 +499,14 @@ fn is_enum(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool {
/// valid type alias. In particular, this function checks for uses of `typing.Any`, `None`,
/// parameterized generics, and PEP 604-style unions.
fn is_annotatable_type_alias(value: &Expr, semantic: &SemanticModel) -> bool {
matches!(value, Expr::Subscript(_) | Expr::NoneLiteral(_))
|| is_valid_pep_604_union(value)
matches!(
value,
Expr::Subscript(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::None,
..
}),
) || is_valid_pep_604_union(value)
|| semantic.match_typing_expr(value, "Any")
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -51,8 +51,14 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) {
}
let length = match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
..
}) => s.chars().count(),
Expr::Constant(ast::ExprConstant {
value: Constant::Bytes(bytes),
..
}) => bytes.len(),
_ => return,
};
if length <= 50 {

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -123,7 +123,11 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
return;
}
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = right {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = right
{
// Other values are possible but we don't need them right now.
// This protects against typos.
if checker.enabled(Rule::UnrecognizedPlatformName) {

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{self as ast, CmpOp, Expr, Int};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -241,8 +241,8 @@ impl ExpectedComparator {
step: None,
..
}) => {
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(upper),
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(upper),
..
}) = upper.as_ref()
{
@@ -254,8 +254,8 @@ impl ExpectedComparator {
}
}
}
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(Int::ZERO),
Expr::Constant(ast::ExprConstant {
value: Constant::Int(Int::ZERO),
..
}) => {
return Some(ExpectedComparator::MajorDigit);
@@ -271,8 +271,8 @@ impl ExpectedComparator {
fn is_int_constant(expr: &Expr) -> bool {
matches!(
expr,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
Expr::Constant(ast::ExprConstant {
value: ast::Constant::Int(_),
..
})
)

View File

@@ -491,8 +491,7 @@ fn to_pytest_raises_args<'a>(
/// PT015
pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
let truthiness = Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id));
if matches!(truthiness, Truthiness::False | Truthiness::Falsey) {
if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() {
checker
.diagnostics
.push(Diagnostic::new(PytestAssertAlwaysFalse, stmt.range()));

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Decorator, Expr, Keyword};
use ruff_python_ast::{self as ast, Constant, Decorator, Expr, Keyword};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::helpers::map_callable;
@@ -44,7 +44,11 @@ pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticMo
}
pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &keyword.value {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = &keyword.value
{
value == literal
} else {
false
@@ -53,8 +57,11 @@ pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool {
pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::NoneLiteral(_) => true,
Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
..
}) => string.is_empty(),
Expr::Constant(constant) if constant.value.is_none() => true,
Expr::FString(ast::ExprFString { values, .. }) => {
values.iter().all(is_empty_or_null_string)
}

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