Compare commits

...

26 Commits

Author SHA1 Message Date
Zanie
1880cb45df Commit test rules :) 2023-09-13 16:17:30 -05:00
Zanie
df8a3db939 Update integration tests to use test rules 2023-09-13 16:00:30 -05:00
Zanie
595207b853 Swap the order of nursery and preview rules 2023-09-13 15:49:55 -05:00
Zanie
97654a9911 Fix invocations 2023-09-13 15:48:02 -05:00
Zanie
eccda3d3ed Use feature instead of test cfg 2023-09-13 15:33:36 -05:00
Zanie
daa9b4db72 WIP: Add rules only enabled during testing
Failing with... the trait `std::convert::From<codes::Rule>` is not implemented for `DiagnosticKind`
2023-09-13 15:33:36 -05:00
Zanie Blue
ebd1b296fd Add warnings for nursery and preview rule selection (#7210)
## Summary

Adds warnings for cases where:
- A selector does not include any rules because preview is disabled
- A nursery rule is selected without the preview flag

## Test plan

Add integration tests
2023-09-13 15:29:58 -05:00
Zanie Blue
1373e1c395 Update release workflow to checkout the given sha (#7279) 2023-09-13 14:59:41 -05:00
Zanie Blue
4bff397318 Move FURB145 from nursery to preview (#7364)
Moves the new rule from nursery to preview for the upcoming release.

Adds new test coverage for selection of a single preview rule and fixes
a bug where preview rules were incorrectly selectable with exact codes.
2023-09-13 14:54:28 -05:00
Charlie Marsh
5347df4728 Parenthesize single-generator arguments when adding reverse keyword (#7365)
Closes https://github.com/astral-sh/ruff/issues/7289.
2023-09-13 19:40:45 +00:00
Tom Kuson
ebe9c03545 [refurb] Implement no-slice-copy (FURB145) (#7007)
## Summary

Implement
[`no-slice-copy`](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/no_slice_copy.py)
as `slice-copy` (`FURB145`).

Related to #1348.

## Test Plan

`cargo test`
2023-09-13 17:31:15 +00:00
Charlie Marsh
b0cbcd3dfa Update deprecated-import lists based on recent typing-extension release (#7356)
## Summary

Generated by running
f3cff244e3/testing/generate-typing-rewrite-info (L14)
with latest `typing-extensions` and manually applying the changes.

Closes https://github.com/astral-sh/ruff/issues/7324.
2023-09-13 13:21:11 -04:00
Charlie Marsh
4df9e07a79 Add a benchmarking script for the formatter CLI (#7340)
## Summary

This PR adds a benchmarking script for the formatter, which benchmarks
the Ruff formatter against Black, yapf, and autopep8.

Three benchmarks are included:

1. Format everything.
2. Format everything, but use a single thread.
3. Format everything, but `--check` (don't write to disk).

There's some nuance in figuring out the right combination of arguments
to each command, but the _main_ nuance is to ensure that we always run
the given formatter (and modify the target repo in-place) prior to
benchmarking it, so that the formatters aren't disadvantaged by the
existing formatting of the target repo. (E.g.: prior to benchmarking
Black's preview style, we need to make sure we format the target repo
with Black's preview style; otherwise, preview style appears much
slower.)

Part of https://github.com/astral-sh/ruff/issues/7309.
2023-09-13 13:15:48 -04:00
Charlie Marsh
f0f7ea7502 Treat whitespace-only line as blank for D411 (#7351)
This better aligns with the definition of "blank line" that we use
throughout the docstring rules.

Closes https://github.com/astral-sh/ruff/issues/7216.
2023-09-13 16:41:12 +00:00
Micha Reiser
4f26002dd5 chore: Upgrade strum (#7337)
## Summary

This PR upgrades `strum` from 0.24.x to 0.25.x. 

The breaking changes are: 
* strum macros now uses syn2
* The `to_string` behavior changed when using `default`. I did a quick search, we aren't using `strum(default)` 


`strum` now has a `#[derive(EnumIs)]` macro that generates `is_` methods. 

## Test Plan

cargo test
2023-09-13 18:33:27 +02:00
dependabot[bot]
d1a9c198e3 Bump path-absolutize from 3.1.0 to 3.1.1 (#7345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 16:21:51 +00:00
dependabot[bot]
7a4f699fba Bump argfile from 0.1.5 to 0.1.6 (#7344)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 16:18:21 +00:00
dependabot[bot]
3fb5418c2c Bump memchr from 2.6.2 to 2.6.3 (#7343)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 18:14:42 +02:00
dependabot[bot]
9fcc009a0c Bump serde_json from 1.0.105 to 1.0.106 (#7342)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 18:13:24 +02:00
Micha Reiser
bf8e5a167b chore: Upgrade walkdir (#7336)
## Summary

The only commit is [api: add follow_root_links() option to WalkDir](dcc527d832) whicih addsa  new option wheter `walkdir` should follow a root symlink or not. 
The new option defaults to `true` which is the same as before. 

## Test Plan

`cargo test`
2023-09-13 18:07:24 +02:00
Micha Reiser
8a001dfc3d chore: Upgrade pyproject-toml crate (#7335)
## Summary

This PR bumps the pyproject-toml crate to 0.7.0. The only difference is that it now depends on indexmap 2. I reviewed the indexmap 2 changes and they don't seem relevant to us. 

I used this opportunity to remove the default features from `serde_with` which removes our indexmap 1 dependency (and some other unused dependencies)

## Test Plan

`cargo test`
2023-09-13 17:55:03 +02:00
Zanie Blue
0823394525 Display nursery rules as preview in documentation (#7341)
This is broken in the last release
2023-09-13 10:46:43 -05:00
Zanie Blue
e15047815c Update Rust toolchain file to use TOML format (#7339)
Ref https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file
Should resolve Dependabot failure at
https://github.com/astral-sh/ruff/network/updates/721380342
Follows #7034
2023-09-13 15:43:03 +00:00
Micha Reiser
7531bb3b21 Upgrade is-macros to 0.3.0 (#7334) 2023-09-13 15:27:40 +02:00
Micha Reiser
2d9b39871f Introduce IndentWidth (#7301) 2023-09-13 14:52:24 +02:00
Micha Reiser
e122a96d27 playground: Respect line-length and preview configuration (#7330) 2023-09-13 12:14:25 +00:00
66 changed files with 1527 additions and 1163 deletions

View File

@@ -7,12 +7,15 @@ on:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "Optionally, the full sha of the commit to be released"
description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
default: ""
type: string
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
- pyproject.toml
# And when we change this workflow itself...
- .github/workflows/release.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -31,6 +34,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -57,6 +62,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -95,6 +102,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -141,6 +150,8 @@ jobs:
arch: x64
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -187,6 +198,8 @@ jobs:
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -244,6 +257,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -297,6 +312,8 @@ jobs:
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -351,6 +368,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -399,6 +418,8 @@ jobs:
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
@@ -410,6 +431,15 @@ jobs:
else
echo "Releasing ${version}"
fi
- name: Check main branch
if: ${{ inputs.sha }}
run: |
# Fetch the main branch since a shallow checkout is used by default
git fetch origin main --unshallow
if ! git branch --contains ${{ inputs.sha }} | grep -E '(^|\s)main$'; then
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check SHA consistency
if: ${{ inputs.sha }}
run: |
@@ -465,6 +495,8 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: git tag
run: |
git config user.email "hey@astral.sh"

132
Cargo.lock generated
View File

@@ -128,10 +128,11 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "argfile"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265f5108974489a217d5098cd81666b60480c8dd67302acbbe7cbdd8aa09d638"
checksum = "1287c4f82a41c5085e65ee337c7934d71ab43d5187740a81fb69129013f6a5f6"
dependencies = [
"fs-err",
"os_str_bytes",
]
@@ -279,8 +280,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"time 0.1.45",
"time",
"wasm-bindgen",
"windows-targets 0.48.5",
]
@@ -624,15 +624,6 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "deranged"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
dependencies = [
"serde",
]
[[package]]
name = "diff"
version = "0.1.13"
@@ -945,12 +936,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexf-parse"
version = "0.2.1"
@@ -1039,17 +1024,6 @@ dependencies = [
"rust-stemmers",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
@@ -1140,15 +1114,15 @@ dependencies = [
[[package]]
name = "is-macro"
version = "0.2.2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20"
checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e"
dependencies = [
"Inflector",
"pmutil",
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.29",
]
[[package]]
@@ -1356,9 +1330,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "memoffset"
@@ -1582,18 +1556,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "path-absolutize"
version = "3.1.0"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de"
checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.1.0"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e"
checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
dependencies = [
"once_cell",
]
@@ -1672,7 +1646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap 2.0.0",
"indexmap",
]
[[package]]
@@ -1739,6 +1713,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "pmutil"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "portable-atomic"
version = "1.4.3"
@@ -1830,11 +1815,11 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee79feaa9d31e1c417e34219e610b67db4e786ce9b49d77dda549640abb9dc5f"
checksum = "569e259cd132eb8cec5df8b672d187c5260f82ad352156b5da9549d4472e64b0"
dependencies = [
"indexmap 1.9.3",
"indexmap",
"pep440_rs",
"pep508_rs",
"serde",
@@ -1848,7 +1833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
dependencies = [
"chrono",
"indexmap 2.0.0",
"indexmap",
"nextest-workspace-hack",
"quick-xml",
"thiserror",
@@ -2013,7 +1998,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fabf0a2e54f711c68c50d49f648a1a8a37adcb57353f518ac4df374f0788f42"
dependencies = [
"pmutil",
"pmutil 0.5.3",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -2722,9 +2707,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [
"itoa",
"ryu",
@@ -2755,15 +2740,8 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.28",
]
[[package]]
@@ -2853,24 +2831,24 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
"syn 2.0.29",
]
[[package]]
@@ -3052,34 +3030,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
dependencies = [
"deranged",
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -3141,7 +3091,7 @@ version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.0.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
@@ -3431,9 +3381,9 @@ dependencies = [
[[package]]
name = "walkdir"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",

View File

@@ -22,27 +22,27 @@ glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.31.0", feature = ["filters", "glob"] }
is-macro = { version = "0.2.2" }
is-macro = { version = "0.3.0" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
memchr = "2.5.0"
memchr = "2.6.3"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.0.14" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93" }
serde_json = { version = "1.0.106" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.10.0" }
static_assertions = "1.1.0"
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }
syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
thiserror = { version = "1.0.43" }

View File

@@ -56,7 +56,7 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
pyproject-toml = { version = "0.6.0" }
pyproject-toml = { version = "0.7.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
@@ -89,3 +89,5 @@ default = []
schemars = ["dep:schemars"]
# Enables the UnreachableCode rule
unreachable-code = []
# Enables rules for internal integration tests
test-rules = []

View File

@@ -8,6 +8,11 @@ reversed(sorted(x, key=lambda e: e, reverse=True))
reversed(sorted(x, reverse=True, key=lambda e: e))
reversed(sorted(x, reverse=False))
# Regression test for: https://github.com/astral-sh/ruff/issues/7289
reversed(sorted(i for i in range(42)))
reversed(sorted((i for i in range(42)), reverse=True))
def reversed(*args, **kwargs):
return None

View File

@@ -529,3 +529,16 @@ def replace_equals_with_dash2():
Parameters
===========
"""
@expect(_D213)
def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""

View File

@@ -74,6 +74,8 @@ from typing import Collection
from typing import AsyncGenerator
from typing import Reversible
from typing import Generator
from typing import Callable
from typing import cast
# OK
from a import b

View File

@@ -0,0 +1,21 @@
l = [1, 2, 3, 4, 5]
# Errors.
a = l[:]
b, c = 1, l[:]
d, e = l[:], 1
m = l[::]
l[:]
print(l[:])
# False negatives.
aa = a[:] # Type inference.
# OK.
t = (1, 2, 3, 4, 5)
f = t[:] # t.copy() is not supported.
g = l[1:3]
h = l[1:]
i = l[:3]
j = l[1:3:2]
k = l[::2]

View File

@@ -16,7 +16,7 @@ use crate::rules::{
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging_format,
flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self, flake8_simplify,
flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet, pep8_naming, pycodestyle,
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff,
pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -113,10 +113,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, subscript);
}
if checker.enabled(Rule::InvalidIndexType) {
ruff::rules::invalid_index_type(checker, subscript);
}
if checker.settings.rules.enabled(Rule::SliceCopy) {
refurb::rules::slice_copy(checker, subscript);
}
pandas_vet::rules::subscript(checker, value, expr);
}

View File

@@ -865,6 +865,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(feature = "test-rules")]
(Ruff, "900") => (RuleGroup::Unspecified, rules::ruff::rules::StableTestRule),
#[cfg(feature = "test-rules")]
(Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule),
#[cfg(feature = "test-rules")]
#[allow(deprecated)]
(Ruff, "912") => (RuleGroup::Nursery, rules::ruff::rules::NurseryTestRule),
// flake8-django
(Flake8Django, "001") => (RuleGroup::Unspecified, rules::flake8_django::rules::DjangoNullableModelStringField),
@@ -916,6 +923,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
_ => return None,
})

View File

@@ -8,16 +8,6 @@ use itertools::Itertools;
use log::error;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
use crate::autofix::{fix_file, FixResult};
use crate::checkers::ast::check_ast;
use crate::checkers::filesystem::check_file_path;
@@ -35,6 +25,15 @@ use crate::rules::pycodestyle;
use crate::settings::{flags, Settings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
@@ -213,6 +212,31 @@ pub fn check_path(
));
}
// Raise violations for internal test rules
#[cfg(feature = "test-rules")]
{
if settings.rules.enabled(Rule::StableTestRule) {
diagnostics.push(Diagnostic::new(
crate::rules::ruff::rules::StableTestRule,
ruff_text_size::TextRange::default(),
));
}
if settings.rules.enabled(Rule::PreviewTestRule) {
diagnostics.push(Diagnostic::new(
crate::rules::ruff::rules::PreviewTestRule,
ruff_text_size::TextRange::default(),
));
}
if settings.rules.enabled(Rule::NurseryTestRule) {
diagnostics.push(Diagnostic::new(
crate::rules::ruff::rules::NurseryTestRule,
ruff_text_size::TextRange::default(),
));
}
}
// Ignore diagnostics based on per-file-ignores.
if !diagnostics.is_empty() && !settings.per_file_ignores.is_empty() {
let ignores = fs::ignores_from_path(path, &settings.per_file_ignores);

View File

@@ -212,7 +212,7 @@ impl RuleSelector {
// Always include rules that are not in preview or the nursery
!(rule.is_preview() || rule.is_nursery())
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|| (matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. }) && rule.is_nursery())
|| ((matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
// Enabling preview includes all preview or nursery rules
|| preview.is_enabled()
})

View File

@@ -718,7 +718,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
if outer_name.value == "list" {
tree = Expression::Call(Box::new((*inner_call).clone()));
} else {
// If the `reverse` argument is used
// If the `reverse` argument is used...
let args = if inner_call.args.iter().any(|arg| {
matches!(
arg.keyword,
@@ -770,6 +770,28 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
.collect_vec()
} else {
let mut args = inner_call.args.clone();
// If necessary, parenthesize a generator expression, as a generator expression must
// be parenthesized if it's not a solitary argument. For example, given:
// ```python
// reversed(sorted(i for i in range(42)))
// ```
// Rewrite as:
// ```python
// sorted((i for i in range(42)), reverse=True)
// ```
if let [arg] = args.as_mut_slice() {
if matches!(arg.value, Expression::GeneratorExp(_)) {
if arg.value.lpar().is_empty() && arg.value.rpar().is_empty() {
arg.value = arg
.value
.clone()
.with_parens(LeftParen::default(), RightParen::default());
}
}
}
// Add the `reverse=True` argument.
args.push(Arg {
value: Expression::Name(Box::new(Name {
value: "True",

View File

@@ -123,7 +123,7 @@ C413.py:8:1: C413 [*] Unnecessary `reversed` call around `sorted()`
8 |+sorted(x, reverse=False, key=lambda e: e)
9 9 | reversed(sorted(x, reverse=False))
10 10 |
11 11 | def reversed(*args, **kwargs):
11 11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
@@ -132,7 +132,7 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
9 | reversed(sorted(x, reverse=False))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
10 |
11 | def reversed(*args, **kwargs):
11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
= help: Remove unnecessary `reversed` call
@@ -143,7 +143,45 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
9 |-reversed(sorted(x, reverse=False))
9 |+sorted(x, reverse=True)
10 10 |
11 11 | def reversed(*args, **kwargs):
12 12 | return None
11 11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
12 12 | reversed(sorted(i for i in range(42)))
C413.py:12:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
12 | reversed(sorted(i for i in range(42)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
13 | reversed(sorted((i for i in range(42)), reverse=True))
|
= help: Remove unnecessary `reversed` call
Suggested fix
9 9 | reversed(sorted(x, reverse=False))
10 10 |
11 11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
12 |-reversed(sorted(i for i in range(42)))
12 |+sorted((i for i in range(42)), reverse=True)
13 13 | reversed(sorted((i for i in range(42)), reverse=True))
14 14 |
15 15 |
C413.py:13:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
12 | reversed(sorted(i for i in range(42)))
13 | reversed(sorted((i for i in range(42)), reverse=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
= help: Remove unnecessary `reversed` call
Suggested fix
10 10 |
11 11 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
12 12 | reversed(sorted(i for i in range(42)))
13 |-reversed(sorted((i for i in range(42)), reverse=True))
13 |+sorted((i for i in range(42)), reverse=False)
14 14 |
15 15 |
16 16 | def reversed(*args, **kwargs):

View File

@@ -1691,7 +1691,10 @@ fn common_section(
}
if checker.enabled(Rule::NoBlankLineBeforeSection) {
if !context.previous_line().is_some_and(str::is_empty) {
if !context
.previous_line()
.is_some_and(|line| line.trim().is_empty())
{
let mut diagnostic = Diagnostic::new(
NoBlankLineBeforeSection {
name: context.section_name().to_string(),

View File

@@ -496,5 +496,6 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
530 |+ ----------
530 531 | ===========
531 532 | """
532 533 |

View File

@@ -138,16 +138,52 @@ const PIPES_TO_SHLEX: &[&str] = &["quote"];
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[
"AbstractSet",
"AnyStr",
"AsyncIterable",
"AsyncIterator",
"Awaitable",
"BinaryIO",
"Callable",
"ClassVar",
"Collection",
"Container",
"ContextManager",
"Coroutine",
"DefaultDict",
"Dict",
"FrozenSet",
"Generator",
"Generic",
"Hashable",
"IO",
"ItemsView",
"Iterable",
"Iterator",
"KeysView",
"List",
"Mapping",
"MappingView",
"Match",
"MutableMapping",
"MutableSequence",
"MutableSet",
"Optional",
"Pattern",
"Reversible",
"Sequence",
"Set",
"Sized",
"TYPE_CHECKING",
"Text",
"TextIO",
"Tuple",
"Type",
"Union",
"ValuesView",
"cast",
"no_type_check",
"no_type_check_decorator",
// Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and
// optimizations,
// "NewType",
@@ -165,6 +201,7 @@ const TYPING_EXTENSIONS_TO_TYPING_37: &[&str] = &[
"ChainMap",
"Counter",
"Deque",
"ForwardRef",
"NoReturn",
];
@@ -287,6 +324,18 @@ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
"NamedTuple",
// Introduced in Python 3.8, but `typing_extensions` backports a ton of optimizations that were
// added in Python 3.12.
"Protocol",
"SupportsAbs",
"SupportsBytes",
"SupportsComplex",
"SupportsFloat",
"SupportsInt",
"SupportsRound",
"TypedDict",
"Unpack",
// Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument,
// which was introduced in Python 3.12.
"dataclass_transform",

View File

@@ -931,7 +931,7 @@ UP035.py:74:1: UP035 [*] Import from `collections.abc` instead: `AsyncGenerator`
74 |+from collections.abc import AsyncGenerator
75 75 | from typing import Reversible
76 76 | from typing import Generator
77 77 |
77 77 | from typing import Callable
UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
|
@@ -940,6 +940,7 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
75 | from typing import Reversible
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
76 | from typing import Generator
77 | from typing import Callable
|
= help: Import from `collections.abc`
@@ -950,8 +951,8 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
75 |-from typing import Reversible
75 |+from collections.abc import Reversible
76 76 | from typing import Generator
77 77 |
78 78 | # OK
77 77 | from typing import Callable
78 78 | from typing import cast
UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
|
@@ -959,8 +960,8 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
75 | from typing import Reversible
76 | from typing import Generator
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
77 |
78 | # OK
77 | from typing import Callable
78 | from typing import cast
|
= help: Import from `collections.abc`
@@ -970,23 +971,63 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
75 75 | from typing import Reversible
76 |-from typing import Generator
76 |+from collections.abc import Generator
77 77 |
78 78 | # OK
79 79 | from a import b
77 77 | from typing import Callable
78 78 | from typing import cast
79 79 |
UP035.py:88:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
UP035.py:77:1: UP035 [*] Import from `collections.abc` instead: `Callable`
|
87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 | from typing_extensions import dataclass_transform
75 | from typing import Reversible
76 | from typing import Generator
77 | from typing import Callable
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
78 | from typing import cast
|
= help: Import from `collections.abc`
Suggested fix
74 74 | from typing import AsyncGenerator
75 75 | from typing import Reversible
76 76 | from typing import Generator
77 |-from typing import Callable
77 |+from collections.abc import Callable
78 78 | from typing import cast
79 79 |
80 80 | # OK
UP035.py:87:1: UP035 [*] Import from `typing` instead: `NamedTuple`
|
86 | # Ok: `typing_extensions` contains backported improvements.
87 | from typing_extensions import NamedTuple
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
88 |
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
= help: Import from `typing`
Suggested fix
84 84 | from typing_extensions import SupportsIndex
85 85 |
86 86 | # Ok: `typing_extensions` contains backported improvements.
87 |-from typing_extensions import NamedTuple
87 |+from typing import NamedTuple
88 88 |
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 90 | from typing_extensions import dataclass_transform
UP035.py:90:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 | from typing_extensions import dataclass_transform
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
= help: Import from `typing`
Suggested fix
85 85 | from typing_extensions import NamedTuple
86 86 |
87 87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 |-from typing_extensions import dataclass_transform
88 |+from typing import dataclass_transform
87 87 | from typing_extensions import NamedTuple
88 88 |
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 |-from typing_extensions import dataclass_transform
90 |+from typing import dataclass_transform

View File

@@ -0,0 +1,36 @@
use ruff_python_ast as ast;
use ruff_python_codegen::Generator;
use ruff_text_size::TextRange;
/// Format a code snippet to call `name.method()`.
pub(super) fn generate_method_call(name: &str, method: &str, generator: Generator) -> String {
// Construct `name`.
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Construct `name.method`.
let attr = ast::ExprAttribute {
value: Box::new(var.into()),
attr: ast::Identifier::new(method.to_string(), TextRange::default()),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make it into a call `name.method()`
let call = ast::ExprCall {
func: Box::new(attr.into()),
arguments: ast::Arguments {
args: vec![],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
};
// And finally, turn it into a statement.
let stmt = ast::StmtExpr {
value: Box::new(call.into()),
range: TextRange::default(),
};
generator.stmt(&stmt.into())
}

View File

@@ -1,5 +1,6 @@
//! Rules from [refurb](https://pypi.org/project/refurb/)/
mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -16,6 +17,7 @@ mod tests {
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -18,6 +18,11 @@ use crate::registry::AsRule;
/// If an element should be removed from a set if it is present, it is more
/// succinct and idiomatic to use `discard`.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect sets that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// nums = {123, 456}

View File

@@ -1,13 +1,13 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Generator;
use ruff_python_semantic::analyze::typing::{is_dict, is_list};
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::refurb::helpers::generate_method_call;
/// ## What it does
/// Checks for `del` statements that delete the entire slice of a list or
@@ -17,6 +17,11 @@ use crate::registry::AsRule;
/// It's is faster and more succinct to remove all items via the `clear()`
/// method.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists and dictionaries that are instantiated as
/// literals or annotated with a type annotation.
///
/// ## Example
/// ```python
/// names = {"key": "value"}
@@ -65,7 +70,7 @@ pub(crate) fn delete_full_slice(checker: &mut Checker, delete: &ast::StmtDelete)
// Fix is only supported for single-target deletions.
if checker.patch(diagnostic.kind.rule()) && delete.targets.len() == 1 {
let replacement = make_suggestion(name, checker.generator());
let replacement = generate_method_call(name, "clear", checker.generator());
diagnostic.set_fix(Fix::suggested(Edit::replacement(
replacement,
delete.start(),
@@ -118,38 +123,3 @@ fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a
// Name is needed for the fix suggestion.
Some(name)
}
/// Make fix suggestion for the given name, ie `name.clear()`.
fn make_suggestion(name: &str, generator: Generator) -> String {
// Here we construct `var.clear()`
//
// And start with construction of `var`
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make `var.clear`.
let attr = ast::ExprAttribute {
value: Box::new(var.into()),
attr: ast::Identifier::new("clear".to_string(), TextRange::default()),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make it into a call `var.clear()`
let call = ast::ExprCall {
func: Box::new(attr.into()),
arguments: ast::Arguments {
args: vec![],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
};
// And finally, turn it into a statement.
let stmt = ast::StmtExpr {
value: Box::new(call.into()),
range: TextRange::default(),
};
generator.stmt(&stmt.into())
}

View File

@@ -1,7 +1,9 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use repeated_append::*;
pub(crate) use slice_copy::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod repeated_append;
mod slice_copy;

View File

@@ -21,6 +21,11 @@ use crate::registry::AsRule;
/// a single `extend`. Each `append` resizes the list individually, whereas an
/// `extend` can resize the list once for all elements.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// nums = [1, 2, 3]

View File

@@ -0,0 +1,109 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::is_list;
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::refurb::helpers::generate_method_call;
/// ## What it does
/// Checks for unbounded slice expressions to copy a list.
///
/// ## Why is this bad?
/// The `list#copy` method is more readable and consistent with copying other
/// types.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// a = [1, 2, 3]
/// b = a[:]
/// ```
///
/// Use instead:
/// ```python
/// a = [1, 2, 3]
/// b = a.copy()
/// ```
///
/// ## References
/// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types)
#[violation]
pub struct SliceCopy;
impl Violation for SliceCopy {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Prefer `copy` method over slicing")
}
fn autofix_title(&self) -> Option<String> {
Some("Replace with `copy()`".to_string())
}
}
/// FURB145
pub(crate) fn slice_copy(checker: &mut Checker, subscript: &ast::ExprSubscript) {
if subscript.ctx.is_store() || subscript.ctx.is_del() {
return;
}
let Some(name) = match_list_full_slice(subscript, checker.semantic()) else {
return;
};
let mut diagnostic = Diagnostic::new(SliceCopy, subscript.range());
if checker.patch(diagnostic.kind.rule()) {
let replacement = generate_method_call(name, "copy", checker.generator());
diagnostic.set_fix(Fix::suggested(Edit::replacement(
replacement,
subscript.start(),
subscript.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
/// Matches `obj[:]` where `obj` is a list.
fn match_list_full_slice<'a>(
subscript: &'a ast::ExprSubscript,
semantic: &SemanticModel,
) -> Option<&'a str> {
// Check that it is `obj[:]`.
if !matches!(
subscript.slice.as_ref(),
Expr::Slice(ast::ExprSlice {
lower: None,
upper: None,
step: None,
range: _,
})
) {
return None;
}
let ast::ExprName { id, .. } = subscript.value.as_name_expr()?;
// Check that `obj` is a list.
let scope = semantic.current_scope();
let bindings: Vec<&Binding> = scope
.get_all(id)
.map(|binding_id| semantic.binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
return None;
};
if !is_list(binding, semantic) {
return None;
}
Some(id)
}

View File

@@ -0,0 +1,128 @@
---
source: crates/ruff/src/rules/refurb/mod.rs
---
FURB145.py:4:5: FURB145 [*] Prefer `copy` method over slicing
|
3 | # Errors.
4 | a = l[:]
| ^^^^ FURB145
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
|
= help: Replace with `copy()`
Suggested fix
1 1 | l = [1, 2, 3, 4, 5]
2 2 |
3 3 | # Errors.
4 |-a = l[:]
4 |+a = l.copy()
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 7 | m = l[::]
FURB145.py:5:11: FURB145 [*] Prefer `copy` method over slicing
|
3 | # Errors.
4 | a = l[:]
5 | b, c = 1, l[:]
| ^^^^ FURB145
6 | d, e = l[:], 1
7 | m = l[::]
|
= help: Replace with `copy()`
Suggested fix
2 2 |
3 3 | # Errors.
4 4 | a = l[:]
5 |-b, c = 1, l[:]
5 |+b, c = 1, l.copy()
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 8 | l[:]
FURB145.py:6:8: FURB145 [*] Prefer `copy` method over slicing
|
4 | a = l[:]
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
| ^^^^ FURB145
7 | m = l[::]
8 | l[:]
|
= help: Replace with `copy()`
Suggested fix
3 3 | # Errors.
4 4 | a = l[:]
5 5 | b, c = 1, l[:]
6 |-d, e = l[:], 1
6 |+d, e = l.copy(), 1
7 7 | m = l[::]
8 8 | l[:]
9 9 | print(l[:])
FURB145.py:7:5: FURB145 [*] Prefer `copy` method over slicing
|
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
7 | m = l[::]
| ^^^^^ FURB145
8 | l[:]
9 | print(l[:])
|
= help: Replace with `copy()`
Suggested fix
4 4 | a = l[:]
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 |-m = l[::]
7 |+m = l.copy()
8 8 | l[:]
9 9 | print(l[:])
10 10 |
FURB145.py:8:1: FURB145 [*] Prefer `copy` method over slicing
|
6 | d, e = l[:], 1
7 | m = l[::]
8 | l[:]
| ^^^^ FURB145
9 | print(l[:])
|
= help: Replace with `copy()`
Suggested fix
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 |-l[:]
8 |+l.copy()
9 9 | print(l[:])
10 10 |
11 11 | # False negatives.
FURB145.py:9:7: FURB145 [*] Prefer `copy` method over slicing
|
7 | m = l[::]
8 | l[:]
9 | print(l[:])
| ^^^^ FURB145
10 |
11 | # False negatives.
|
= help: Replace with `copy()`
Suggested fix
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 8 | l[:]
9 |-print(l[:])
9 |+print(l.copy())
10 10 |
11 11 | # False negatives.
12 12 | aa = a[:] # Type inference.

View File

@@ -10,6 +10,8 @@ pub(crate) use mutable_class_default::*;
pub(crate) use mutable_dataclass_default::*;
pub(crate) use pairwise_over_zipped::*;
pub(crate) use static_key_dict_comprehension::*;
#[cfg(feature = "test-rules")]
pub(crate) use test_rules::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
#[cfg(feature = "unreachable-code")]
pub(crate) use unreachable::*;
@@ -29,6 +31,8 @@ mod mutable_class_default;
mod mutable_dataclass_default;
mod pairwise_over_zipped;
mod static_key_dict_comprehension;
#[cfg(feature = "test-rules")]
mod test_rules;
mod unnecessary_iterable_allocation_for_first_element;
#[cfg(feature = "unreachable-code")]
pub(crate) mod unreachable;

View File

@@ -0,0 +1,83 @@
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct StableTestRule;
impl Violation for StableTestRule {
const AUTOFIX: AutofixKind = AutofixKind::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a stable test rule.")
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct PreviewTestRule;
impl Violation for PreviewTestRule {
const AUTOFIX: AutofixKind = AutofixKind::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a preview test rule.")
}
}
/// ## What it does
/// Fake rule for testing.
///
/// ## Why is this bad?
/// Tests must pass!
///
/// ## Example
/// ```python
/// foo
/// ```
///
/// Use instead:
/// ```python
/// bar
/// ```
#[violation]
pub struct NurseryTestRule;
impl Violation for NurseryTestRule {
const AUTOFIX: AutofixKind = AutofixKind::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Hey this is a nursery test rule.")
}
}

View File

@@ -36,7 +36,7 @@ ruff_text_size = { path = "../ruff_text_size" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
argfile = { version = "0.1.5" }
argfile = { version = "0.1.6" }
bincode = { version = "1.3.3" }
bitflags = { workspace = true }
cachedir = { version = "0.3.0" }
@@ -69,6 +69,7 @@ walkdir = { version = "2.3.2" }
wild = { version = "2" }
[dev-dependencies]
ruff = { path = "../ruff", features = ["clap", "test-rules"] }
assert_cmd = { version = "2.0.8" }
# Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"]}

View File

@@ -249,16 +249,16 @@ fn show_statistics() {
#[test]
fn nursery_prefix() {
// `--select E` should detect E741, but not E225, which is in the nursery.
let args = ["--select", "E"];
// Should only detect RUF900, but not the unstable test rules
let args = ["--select", "RUF9"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
-:1:1: RUF900 Hey this is a stable test rule.
Found 1 error.
----- stderr -----
@@ -267,17 +267,17 @@ fn nursery_prefix() {
#[test]
fn nursery_all() {
// `--select ALL` should detect E741, but not E225, which is in the nursery.
// Should detect RUF900, but not the unstable test rules
let args = ["--select", "ALL"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
-:1:1: D100 Missing docstring in public module
-:1:1: RUF900 Hey this is a stable test rule.
Found 2 errors.
----- stderr -----
@@ -288,18 +288,210 @@ fn nursery_all() {
#[test]
fn nursery_direct() {
// `--select E225` should detect E225.
let args = ["--select", "E225"];
let args = ["--select", "RUF912"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:2: E225 Missing whitespace around operator
-:1:1: RUF912 Hey this is a nursery test rule.
Found 1 error.
----- stderr -----
warning: Selection of nursery rule `RUF912` without the `--preview` flag is deprecated.
"###);
}
#[test]
fn nursery_group_selector() {
// Only nursery rules should be detected e.g. RUF912
let args = ["--select", "NURSERY"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: CPY001 Missing copyright notice at top of file
-:1:1: RUF912 Hey this is a nursery test rule.
Found 2 errors.
----- stderr -----
warning: The `NURSERY` selector has been deprecated. Use the `--preview` flag instead.
"###);
}
#[test]
fn nursery_group_selector_preview_enabled() {
// A warning should be displayed due to deprecated selector usage
let args = ["--select", "NURSERY", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: CPY001 Missing copyright notice at top of file
-:1:1: RUF912 Hey this is a nursery test rule.
Found 2 errors.
----- stderr -----
warning: The `NURSERY` selector has been deprecated. Use the `PREVIEW` selector instead.
"###);
}
#[test]
fn preview_enabled_prefix() {
// All the RUF9XX test rules should be triggered
let args = ["--select", "RUF9", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 3 errors.
----- stderr -----
"###);
}
#[test]
fn preview_enabled_all() {
let args = ["--select", "ALL", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: D100 Missing docstring in public module
-:1:1: CPY001 Missing copyright notice at top of file
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 5 errors.
----- 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`.
"###);
}
#[test]
fn preview_enabled_direct() {
// Should be enabled without warning
let args = ["--select", "RUF911", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: RUF911 Hey this is a preview test rule.
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn preview_disabled_direct() {
// RUF911 is preview not nursery so selecting should be empty
let args = ["--select", "RUF911"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Selection `RUF911` has no effect because the `--preview` flag was not included.
"###);
}
#[test]
fn preview_disabled_prefix_empty() {
// Warns that the selection is empty since all of the RUF91 rules are in preview
let args = ["--select", "RUF91"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Selection `RUF91` has no effect because the `--preview` flag was not included.
"###);
}
#[test]
fn preview_disabled_group_selector() {
// `--select PREVIEW` should warn without the `--preview` flag
let args = ["--select", "PREVIEW"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Selection `PREVIEW` has no effect because the `--preview` flag was not included.
"###);
}
#[test]
fn preview_enabled_group_selector() {
// `--select PREVIEW` is okay with the `--preview` flag and shouldn't warn
let args = ["--select", "PREVIEW", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: CPY001 Missing copyright notice at top of file
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 3 errors.
----- stderr -----
"###);
}
#[test]
fn preview_enabled_group_ignore() {
// Should detect stable and unstable rules, RUF9 is more specific than PREVIEW so ignore has no effect
let args = ["--select", "RUF9", "--ignore", "PREVIEW", "--preview"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(""), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
Found 3 errors.
----- stderr -----
"###);
}

View File

@@ -25,7 +25,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
}
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
};
let preview_token = if rule.is_preview() {
let preview_token = if rule.is_preview() || rule.is_nursery() {
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
} else {
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")

View File

@@ -361,7 +361,7 @@ where
f.write_element(FormatElement::Text {
text: self.text.to_string().into_boxed_str(),
text_width: TextWidth::from_text(self.text, f.options().tab_width()),
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
});
Ok(())
@@ -393,8 +393,10 @@ where
let slice = source_code.slice(self.range);
debug_assert_no_newlines(slice.text(source_code));
let text_width =
TextWidth::from_text(slice.text(source_code), f.context().options().tab_width());
let text_width = TextWidth::from_text(
slice.text(source_code),
f.context().options().indent_width(),
);
f.write_element(FormatElement::SourceCodeSlice { slice, text_width });
@@ -917,8 +919,10 @@ where
/// use ruff_formatter::prelude::*;
///
/// # fn main() -> FormatResult<()> {
/// use ruff_formatter::IndentWidth;
/// let context = SimpleFormatContext::new(SimpleFormatOptions {
/// indent_style: IndentStyle::Space(4),
/// indent_style: IndentStyle::Space,
/// indent_width: IndentWidth::try_from(4).unwrap(),
/// ..SimpleFormatOptions::default()
/// });
///

View File

@@ -10,7 +10,7 @@ use unicode_width::UnicodeWidthChar;
use crate::format_element::tag::{GroupMode, LabelId, Tag};
use crate::source_code::SourceCodeSlice;
use crate::{TabWidth, TagKind};
use crate::{IndentWidth, TagKind};
use ruff_text_size::TextSize;
/// Language agnostic IR for formatting source code.
@@ -432,12 +432,12 @@ pub enum TextWidth {
}
impl TextWidth {
pub fn from_text(text: &str, tab_width: TabWidth) -> TextWidth {
pub fn from_text(text: &str, indent_width: IndentWidth) -> TextWidth {
let mut width = 0u32;
for c in text.chars() {
let char_width = match c {
'\t' => tab_width.value(),
'\t' => indent_width.value(),
'\n' => return TextWidth::Multiline,
#[allow(clippy::cast_possible_truncation)]
c => c.width().unwrap_or(0) as u32,

View File

@@ -9,7 +9,7 @@ use crate::prelude::*;
use crate::source_code::SourceCode;
use crate::{
format, write, BufferExtensions, Format, FormatContext, FormatElement, FormatOptions,
FormatResult, Formatter, IndentStyle, LineWidth, PrinterOptions, TabWidth,
FormatResult, Formatter, IndentStyle, IndentWidth, LineWidth, PrinterOptions,
};
use super::tag::Tag;
@@ -213,11 +213,11 @@ struct IrFormatOptions;
impl FormatOptions for IrFormatOptions {
fn indent_style(&self) -> IndentStyle {
IndentStyle::Space(2)
IndentStyle::Space
}
fn tab_width(&self) -> TabWidth {
TabWidth::default()
fn indent_width(&self) -> IndentWidth {
IndentWidth::default()
}
fn line_width(&self) -> LineWidth {
@@ -227,7 +227,7 @@ impl FormatOptions for IrFormatOptions {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
line_width: self.line_width(),
indent_style: IndentStyle::Space(2),
indent_style: IndentStyle::Space,
..PrinterOptions::default()
}
}

View File

@@ -52,23 +52,20 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
pub use group_id::GroupId;
use ruff_text_size::{TextRange, TextSize};
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum IndentStyle {
/// Tab
/// Use tabs to indent code.
#[default]
Tab,
/// Space, with its quantity
Space(u8),
/// Use [`IndentWidth`] spaces to indent code.
Space,
}
impl IndentStyle {
pub const DEFAULT_SPACES: u8 = 2;
/// Returns `true` if this is an [`IndentStyle::Tab`].
pub const fn is_tab(&self) -> bool {
matches!(self, IndentStyle::Tab)
@@ -76,58 +73,42 @@ impl IndentStyle {
/// Returns `true` if this is an [`IndentStyle::Space`].
pub const fn is_space(&self) -> bool {
matches!(self, IndentStyle::Space(_))
}
}
impl FromStr for IndentStyle {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"tab" | "Tabs" => Ok(Self::Tab),
"space" | "Spaces" => Ok(Self::Space(IndentStyle::DEFAULT_SPACES)),
// TODO: replace this error with a diagnostic
v => {
let v = v.strip_prefix("Spaces, size: ").unwrap_or(v);
u8::from_str(v)
.map(Self::Space)
.map_err(|_| "Value not supported for IndentStyle")
}
}
matches!(self, IndentStyle::Space)
}
}
impl std::fmt::Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndentStyle::Tab => std::write!(f, "Tab"),
IndentStyle::Space(size) => std::write!(f, "Spaces, size: {size}"),
IndentStyle::Tab => std::write!(f, "tab"),
IndentStyle::Space => std::write!(f, "space"),
}
}
}
/// The visual width of a `\t` character.
/// The visual width of a indentation.
///
/// Determines the visual width of a tab character (`\t`) and the number of
/// spaces per indent when using [`IndentStyle::Space`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabWidth(NonZeroU8);
pub struct IndentWidth(NonZeroU8);
impl TabWidth {
impl IndentWidth {
/// Return the numeric value for this [`LineWidth`]
pub const fn value(&self) -> u32 {
self.0.get() as u32
}
}
impl Default for TabWidth {
impl Default for IndentWidth {
fn default() -> Self {
Self(NonZeroU8::new(2).unwrap())
}
}
impl TryFrom<u8> for TabWidth {
impl TryFrom<u8> for IndentWidth {
type Error = TryFromIntError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
@@ -196,16 +177,8 @@ pub trait FormatOptions {
/// The indent style.
fn indent_style(&self) -> IndentStyle;
/// The visual width of a tab character.
fn tab_width(&self) -> TabWidth;
/// The visual width of an indent
fn indent_width(&self) -> u32 {
match self.indent_style() {
IndentStyle::Tab => self.tab_width().value(),
IndentStyle::Space(spaces) => u32::from(spaces),
}
}
fn indent_width(&self) -> IndentWidth;
/// What's the max width of a line. Defaults to 80.
fn line_width(&self) -> LineWidth;
@@ -250,6 +223,7 @@ impl FormatContext for SimpleFormatContext {
#[derive(Debug, Default, Eq, PartialEq, Clone)]
pub struct SimpleFormatOptions {
pub indent_style: IndentStyle,
pub indent_width: IndentWidth,
pub line_width: LineWidth,
}
@@ -258,8 +232,8 @@ impl FormatOptions for SimpleFormatOptions {
self.indent_style
}
fn tab_width(&self) -> TabWidth {
TabWidth::default()
fn indent_width(&self) -> IndentWidth {
self.indent_width
}
fn line_width(&self) -> LineWidth {
@@ -270,6 +244,7 @@ impl FormatOptions for SimpleFormatOptions {
PrinterOptions {
line_width: self.line_width,
indent_style: self.indent_style,
indent_width: self.indent_width,
source_map_generation: SourceMapGeneration::Enabled,
..PrinterOptions::default()
}

View File

@@ -367,7 +367,7 @@ impl<'a> Printer<'a> {
if !self.state.pending_indent.is_empty() {
let (indent_char, repeat_count) = match self.options.indent_style() {
IndentStyle::Tab => ('\t', 1),
IndentStyle::Space(count) => (' ', count),
IndentStyle::Space => (' ', self.options.indent_width()),
};
let indent = std::mem::take(&mut self.state.pending_indent);
@@ -764,7 +764,7 @@ impl<'a> Printer<'a> {
#[allow(clippy::cast_possible_truncation)]
let char_width = if char == '\t' {
self.options.tab_width.value()
self.options.indent_width.value()
} else {
// SAFETY: A u32 is sufficient to represent the width of a file <= 4GB
char.width().unwrap_or(0) as u32
@@ -1347,7 +1347,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
} else {
for c in text.chars() {
let char_width = match c {
'\t' => self.options().tab_width.value(),
'\t' => self.options().indent_width.value(),
'\n' => {
if self.must_be_flat {
return Fits::No;
@@ -1501,7 +1501,7 @@ mod tests {
use crate::printer::{LineEnding, Printer, PrinterOptions};
use crate::source_code::SourceCode;
use crate::{
format_args, write, Document, FormatState, IndentStyle, LineWidth, Printed, TabWidth,
format_args, write, Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed,
VecBuffer,
};
@@ -1509,7 +1509,7 @@ mod tests {
format_with_options(
root,
PrinterOptions {
indent_style: IndentStyle::Space(2),
indent_style: IndentStyle::Space,
..PrinterOptions::default()
},
)
@@ -1653,7 +1653,7 @@ two lines`,
fn it_use_the_indent_character_specified_in_the_options() {
let options = PrinterOptions {
indent_style: IndentStyle::Tab,
tab_width: TabWidth::try_from(4).unwrap(),
indent_width: IndentWidth::try_from(4).unwrap(),
line_width: LineWidth::try_from(19).unwrap(),
..PrinterOptions::default()
};

View File

@@ -1,10 +1,13 @@
use crate::{FormatOptions, IndentStyle, LineWidth, TabWidth};
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct PrinterOptions {
/// Width of a single tab character (does it equal 2, 4, ... spaces?)
pub tab_width: TabWidth,
pub indent_width: IndentWidth,
/// Whether the printer should use tabs or spaces to indent code.
pub indent_style: IndentStyle,
/// What's the max width of a line. Defaults to 80
pub line_width: LineWidth,
@@ -12,9 +15,6 @@ pub struct PrinterOptions {
/// The type of line ending to apply to the printed input
pub line_ending: LineEnding,
/// Whether the printer should use tabs or spaces to indent code and if spaces, by how many.
pub indent_style: IndentStyle,
/// Whether the printer should build a source map that allows mapping positions in the source document
/// to positions in the formatted document.
pub source_map_generation: SourceMapGeneration,
@@ -46,8 +46,8 @@ impl PrinterOptions {
}
#[must_use]
pub fn with_tab_width(mut self, width: TabWidth) -> Self {
self.tab_width = width;
pub fn with_tab_width(mut self, width: IndentWidth) -> Self {
self.indent_width = width;
self
}
@@ -58,10 +58,7 @@ impl PrinterOptions {
/// Width of an indent in characters.
pub(super) const fn indent_width(&self) -> u32 {
match self.indent_style {
IndentStyle::Tab => self.tab_width.value(),
IndentStyle::Space(count) => count as u32,
}
self.indent_width.value()
}
}

View File

@@ -22,7 +22,7 @@ itertools = { workspace = true }
once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { version = "3.0.0" }
serde_with = { version = "3.0.0", default-features = false, features = ["macros"] }
thiserror = { workspace = true }
uuid = { workspace = true }

View File

@@ -1,3 +1,3 @@
{
"tab_width": 8
"indent_width": 4
}

View File

@@ -1,22 +1,18 @@
[
{
"indent_style": {
"Space": 4
},
"tab_width": 8
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": {
"Space": 2
},
"tab_width": 8
"indent_style": "Space",
"indent_width": 2
},
{
"indent_style": "Tab",
"tab_width": 8
"indent_width": 8
},
{
"indent_style": "Tab",
"tab_width": 4
"indent_width": 4
}
]

View File

@@ -1,8 +1,10 @@
[
{
"indent_style": { "Space": 4 }
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": { "Space": 2 }
"indent_style": "Space",
"indent_width": 2
}
]

View File

@@ -1,9 +1,11 @@
[
{
"indent_style": { "Space": 4 }
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": { "Space": 1 }
"indent_style": "Space",
"indent_width": 1
},
{
"indent_style": "Tab"

View File

@@ -1,9 +1,11 @@
[
{
"indent_style": { "Space": 4 }
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": { "Space": 2 }
"indent_style": "Space",
"indent_width": 2
},
{
"indent_style": "Tab"

View File

@@ -1,8 +1,11 @@
[
{
"tab_width": 2
"indent_width": 2
},
{
"tab_width": 4
"indent_width": 4
},
{
"indent_width": 8
}
]

View File

@@ -1,8 +1,8 @@
# Fits with tab width 2
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
# Fits with tab width 4
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
# Fits with tab width 8
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")

View File

@@ -353,7 +353,7 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
} else {
// Start with 2 because of the two leading spaces.
let width = 2u32.saturating_add(
TextWidth::from_text(&normalized_comment, f.options().tab_width())
TextWidth::from_text(&normalized_comment, f.options().indent_width())
.width()
.expect("Expected comment not to contain any newlines")
.value(),

View File

@@ -47,7 +47,7 @@ where
text_len > 5
&& text_len
<= context.options().line_width().value() as usize
- context.options().indent_width() as usize
- context.options().indent_width().value() as usize
}
pub(crate) trait NeedsParentheses {

View File

@@ -2,7 +2,7 @@ use std::borrow::Cow;
use bitflags::bitflags;
use ruff_formatter::{format_args, write, FormatError, FormatOptions, TabWidth};
use ruff_formatter::{format_args, write, FormatError};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{self as ast, Constant, ExprConstant, ExprFString, ExpressionRef};
use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType};
@@ -727,22 +727,20 @@ fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str>
/// For docstring indentation, black counts spaces as 1 and tabs by increasing the indentation up
/// to the next multiple of 8. This is effectively a port of
/// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs),
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61)
fn count_indentation_like_black(line: &str, tab_width: TabWidth) -> TextSize {
let mut indentation = TextSize::default();
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61).
fn indentation_length(line: &str) -> TextSize {
let mut indentation = 0u32;
for char in line.chars() {
if char == '\t' {
// Pad to the next multiple of tab_width
indentation += TextSize::from(
tab_width.value() - (indentation.to_u32().rem_euclid(tab_width.value())),
);
indentation += 8 - (indentation.rem_euclid(8));
} else if char.is_whitespace() {
indentation += char.text_len();
indentation += u32::from(char.text_len());
} else {
return indentation;
break;
}
}
indentation
TextSize::new(indentation)
}
/// Format a docstring by trimming whitespace and adjusting the indentation.
@@ -910,7 +908,7 @@ fn format_docstring(normalized: &NormalizedString, f: &mut PyFormatter) -> Forma
.clone()
// We don't want to count whitespace-only lines as miss-indented
.filter(|line| !line.trim().is_empty())
.map(|line| count_indentation_like_black(line, f.options().tab_width()))
.map(indentation_length)
.min()
.unwrap_or_default();
@@ -952,7 +950,7 @@ fn format_docstring_line(
line: &str,
is_last: bool,
offset: TextSize,
stripped_indentation: TextSize,
stripped_indentation_length: TextSize,
already_normalized: bool,
f: &mut PyFormatter,
) -> FormatResult<()> {
@@ -979,21 +977,20 @@ fn format_docstring_line(
// overindented, in which case we strip the additional whitespace (see example in
// [`format_docstring`] doc comment). We then prepend the in-docstring indentation to the
// string.
let indent_len =
count_indentation_like_black(trim_end, f.options().tab_width()) - stripped_indentation;
let in_docstring_indent = " ".repeat(indent_len.to_usize()) + trim_end.trim_start();
let indent_len = indentation_length(trim_end) - stripped_indentation_length;
let in_docstring_indent = " ".repeat(usize::from(indent_len)) + trim_end.trim_start();
text(&in_docstring_indent, Some(offset)).fmt(f)?;
} else {
// Take the string with the trailing whitespace removed, then also skip the leading
// whitespace
let trimmed_line_range =
TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation);
TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation_length);
if already_normalized {
source_text_slice(trimmed_line_range).fmt(f)?;
} else {
// All indents are ascii spaces, so the slicing is correct
text(
&trim_end[stripped_indentation.to_usize()..],
&trim_end[usize::from(stripped_indentation_length)..],
Some(trimmed_line_range.start()),
)
.fmt(f)?;
@@ -1012,25 +1009,14 @@ fn format_docstring_line(
#[cfg(test)]
mod tests {
use ruff_formatter::TabWidth;
use crate::expression::string::count_indentation_like_black;
use crate::expression::string::indentation_length;
use ruff_text_size::TextSize;
#[test]
fn test_indentation_like_black() {
let tab_width = TabWidth::try_from(8).unwrap();
assert_eq!(
count_indentation_like_black("\t \t \t", tab_width).to_u32(),
24
);
assert_eq!(
count_indentation_like_black("\t \t", tab_width).to_u32(),
24
);
assert_eq!(
count_indentation_like_black("\t\t\t", tab_width).to_u32(),
24
);
assert_eq!(count_indentation_like_black(" ", tab_width).to_u32(), 4);
assert_eq!(indentation_length("\t \t \t"), TextSize::new(24));
assert_eq!(indentation_length("\t \t"), TextSize::new(24));
assert_eq!(indentation_length("\t\t\t"), TextSize::new(24));
assert_eq!(indentation_length(" "), TextSize::new(4));
}
}

View File

@@ -1,5 +1,5 @@
use ruff_formatter::printer::{LineEnding, PrinterOptions, SourceMapGeneration};
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth, TabWidth};
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_python_ast::PySourceType;
use std::path::Path;
use std::str::FromStr;
@@ -25,8 +25,8 @@ pub struct PyFormatOptions {
line_width: LineWidth,
/// The visual width of a tab character.
#[cfg_attr(feature = "serde", serde(default = "default_tab_width"))]
tab_width: TabWidth,
#[cfg_attr(feature = "serde", serde(default = "default_indent_width"))]
indent_width: IndentWidth,
line_ending: LineEnding,
@@ -49,11 +49,11 @@ fn default_line_width() -> LineWidth {
}
fn default_indent_style() -> IndentStyle {
IndentStyle::Space(4)
IndentStyle::Space
}
fn default_tab_width() -> TabWidth {
TabWidth::try_from(4).unwrap()
fn default_indent_width() -> IndentWidth {
IndentWidth::try_from(4).unwrap()
}
impl Default for PyFormatOptions {
@@ -62,7 +62,7 @@ impl Default for PyFormatOptions {
source_type: PySourceType::default(),
indent_style: default_indent_style(),
line_width: default_line_width(),
tab_width: default_tab_width(),
indent_width: default_indent_width(),
quote_style: QuoteStyle::default(),
line_ending: LineEnding::default(),
magic_trailing_comma: MagicTrailingComma::default(),
@@ -110,8 +110,8 @@ impl PyFormatOptions {
}
#[must_use]
pub fn with_tab_width(mut self, tab_width: TabWidth) -> Self {
self.tab_width = tab_width;
pub fn with_indent_width(mut self, indent_width: IndentWidth) -> Self {
self.indent_width = indent_width;
self
}
@@ -157,8 +157,8 @@ impl FormatOptions for PyFormatOptions {
self.indent_style
}
fn tab_width(&self) -> TabWidth {
self.tab_width
fn indent_width(&self) -> IndentWidth {
self.indent_width
}
fn line_width(&self) -> LineWidth {
@@ -167,7 +167,7 @@ impl FormatOptions for PyFormatOptions {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
tab_width: self.tab_width,
indent_width: self.indent_width,
line_width: self.line_width,
line_ending: self.line_ending,
indent_style: self.indent_style,

View File

@@ -253,11 +253,11 @@ impl fmt::Display for DisplayPyOptions<'_> {
f,
r#"indent-style = {indent_style}
line-width = {line_width}
tab-width = {tab_width}
indent-width = {indent_width}
quote-style = {quote_style:?}
magic-trailing-comma = {magic_trailing_comma:?}"#,
indent_style = self.0.indent_style(),
tab_width = self.0.tab_width().value(),
indent_width = self.0.indent_width().value(),
line_width = self.0.line_width().value(),
quote_style = self.0.quote_style(),
magic_trailing_comma = self.0.magic_trailing_comma()

View File

@@ -111,9 +111,9 @@ class TabbedIndent:
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 8
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -224,9 +224,9 @@ class TabbedIndent:
### Output 2
```
indent-style = Spaces, size: 2
indent-style = space
line-width = 88
tab-width = 8
indent-width = 2
quote-style = Double
magic-trailing-comma = Respect
```
@@ -337,9 +337,9 @@ class TabbedIndent:
### Output 3
```
indent-style = Tab
indent-style = tab
line-width = 88
tab-width = 8
indent-width = 8
quote-style = Double
magic-trailing-comma = Respect
```
@@ -450,9 +450,9 @@ class TabbedIndent:
### Output 4
```
indent-style = Tab
indent-style = tab
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -556,7 +556,7 @@ class TabbedIndent:
"""check for correct tabbed formatting
^^^^^^^^^^
Normal indented line
- autor
- autor
"""
```

View File

@@ -129,9 +129,9 @@ test_particular = [
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -277,9 +277,9 @@ test_particular = [
### Output 2
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Single
magic-trailing-comma = Respect
```

View File

@@ -141,9 +141,9 @@ x = (b"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" b"""bbbbbbbbbbbbbbbbbbbbbbbbbbb
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -310,9 +310,9 @@ x = (
### Output 2
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Single
magic-trailing-comma = Respect
```

View File

@@ -28,9 +28,9 @@ def test():
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -60,9 +60,9 @@ def test():
### Output 2
```
indent-style = Spaces, size: 2
indent-style = space
line-width = 88
tab-width = 4
indent-width = 2
quote-style = Double
magic-trailing-comma = Respect
```

View File

@@ -9,9 +9,9 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -22,9 +22,9 @@ magic-trailing-comma = Respect
### Output 2
```
indent-style = Spaces, size: 1
indent-style = space
line-width = 88
tab-width = 4
indent-width = 1
quote-style = Double
magic-trailing-comma = Respect
```
@@ -35,9 +35,9 @@ magic-trailing-comma = Respect
### Output 3
```
indent-style = Tab
indent-style = tab
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```

View File

@@ -24,9 +24,9 @@ not_fixed
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -53,9 +53,9 @@ not_fixed
### Output 2
```
indent-style = Spaces, size: 2
indent-style = space
line-width = 88
tab-width = 4
indent-width = 2
quote-style = Double
magic-trailing-comma = Respect
```
@@ -82,9 +82,9 @@ not_fixed
### Output 3
```
indent-style = Tab
indent-style = tab
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```

View File

@@ -42,9 +42,9 @@ with (a,): # magic trailing comma
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -94,9 +94,9 @@ with (
### Output 2
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Ignore
```

View File

@@ -5,45 +5,42 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/tab_width.
## Input
```py
# Fits with tab width 2
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
# Fits with tab width 4
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
# Fits with tab width 8
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
```
## Outputs
### Output 1
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 2
indent-width = 2
quote-style = Double
magic-trailing-comma = Respect
```
```py
# Fits with tab width 2
(
1
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
)
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
# Fits with tab width 4
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
# Fits with tab width 8
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
```
### Output 2
```
indent-style = Spaces, size: 4
indent-style = space
line-width = 88
tab-width = 4
indent-width = 4
quote-style = Double
magic-trailing-comma = Respect
```
@@ -52,17 +49,41 @@ magic-trailing-comma = Respect
# Fits with tab width 2
(
1
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567"
)
# Fits with tab width 4
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
# Fits with tab width 8
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
```
### Output 3
```
indent-style = space
line-width = 88
indent-width = 8
quote-style = Double
magic-trailing-comma = Respect
```
```py
# Fits with tab width 2
(
1
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567"
)
# Fits with tab width 4
(
1
+ " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
1
+ " 0123456789012345678901234567890123456789012345678901234567890123456789012345"
)
# Fits with tab width 8
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
```

View File

@@ -1,3 +1,4 @@
use std::num::NonZeroU16;
use std::path::Path;
use js_sys::Error;
@@ -8,10 +9,10 @@ use ruff::directives;
use ruff::line_width::{LineLength, TabSize};
use ruff::linter::{check_path, LinterResult};
use ruff::registry::AsRule;
use ruff::settings::types::PythonVersion;
use ruff::settings::types::{PreviewMode, PythonVersion};
use ruff::settings::{defaults, flags, Settings};
use ruff::source_kind::SourceKind;
use ruff_formatter::{FormatResult, Formatted};
use ruff_formatter::{FormatResult, Formatted, LineWidth};
use ruff_python_ast::{Mod, PySourceType};
use ruff_python_codegen::Stylist;
use ruff_python_formatter::{format_node, pretty_comments, PyFormatContext, PyFormatOptions};
@@ -237,7 +238,7 @@ impl Workspace {
pub fn format(&self, contents: &str) -> Result<String, Error> {
let parsed = ParsedModule::from_source(contents)?;
let formatted = parsed.format().map_err(into_error)?;
let formatted = parsed.format(&self.settings).map_err(into_error)?;
let printed = formatted.print().map_err(into_error)?;
Ok(printed.into_code())
@@ -245,7 +246,7 @@ impl Workspace {
pub fn format_ir(&self, contents: &str) -> Result<String, Error> {
let parsed = ParsedModule::from_source(contents)?;
let formatted = parsed.format().map_err(into_error)?;
let formatted = parsed.format(&self.settings).map_err(into_error)?;
Ok(format!("{formatted}"))
}
@@ -298,9 +299,14 @@ impl<'a> ParsedModule<'a> {
})
}
fn format(&self) -> FormatResult<Formatted<PyFormatContext>> {
fn format(&self, settings: &Settings) -> FormatResult<Formatted<PyFormatContext>> {
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
let options = PyFormatOptions::from_source_type(PySourceType::default());
let options = PyFormatOptions::from_source_type(PySourceType::default())
.with_preview(match settings.preview {
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
})
.with_line_width(LineWidth::from(NonZeroU16::from(settings.line_length)));
format_node(
&self.module,

View File

@@ -27,7 +27,7 @@ use ruff::settings::types::{
Version,
};
use ruff::settings::{defaults, resolve_per_file_ignores, AllSettings, CliSettings, Settings};
use ruff::{fs, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION};
use ruff::{fs, warn_user, warn_user_once, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION};
use ruff_cache::cache_dir;
use rustc_hash::{FxHashMap, FxHashSet};
use shellexpand;
@@ -460,7 +460,10 @@ impl Configuration {
let mut carryover_ignores: Option<&[RuleSelector]> = None;
let mut carryover_unfixables: Option<&[RuleSelector]> = None;
// Store selectors for displaying warnings
let mut redirects = FxHashMap::default();
let mut deprecated_nursery_selectors = FxHashSet::default();
let mut ignored_preview_selectors = FxHashSet::default();
for selection in &self.rule_selections {
// If a selection only specifies extend-select we cannot directly
@@ -571,8 +574,7 @@ impl Configuration {
}
}
// We insert redirects into the hashmap so that we
// can warn the users about remapped rule codes.
// Check for selections that require a warning
for selector in selection
.select
.iter()
@@ -583,6 +585,29 @@ impl Configuration {
.chain(selection.unfixable.iter())
.chain(selection.extend_fixable.iter())
{
#[allow(deprecated)]
if matches!(selector, RuleSelector::Nursery) {
let suggestion = if preview.is_disabled() {
"Use the `--preview` flag instead."
} else {
"Use the `PREVIEW` selector instead."
};
warn_user_once!("The `NURSERY` selector has been deprecated. {suggestion}");
}
if preview.is_disabled() {
if let RuleSelector::Rule { prefix, .. } = selector {
if prefix.rules().any(|rule| rule.is_nursery()) {
deprecated_nursery_selectors.insert(selector);
}
}
// Check if the selector is empty because preview mode is disabled
if selector.rules(PreviewMode::Disabled).next().is_none() {
ignored_preview_selectors.insert(selector);
}
}
if let RuleSelector::Prefix {
prefix,
redirected_from: Some(redirect_from),
@@ -603,6 +628,18 @@ impl Configuration {
);
}
for selection in deprecated_nursery_selectors {
let (prefix, code) = selection.prefix_and_code();
warn_user!("Selection of nursery rule `{prefix}{code}` without the `--preview` flag is deprecated.",);
}
for selection in ignored_preview_selectors {
let (prefix, code) = selection.prefix_and_code();
warn_user!(
"Selection `{prefix}{code}` has no effect because the `--preview` flag was not included.",
);
}
let mut rules = RuleTable::empty();
for rule in select_set {
@@ -764,7 +801,7 @@ pub fn resolve_src(src: &[String], project_root: &Path) -> Result<Vec<PathBuf>>
#[cfg(test)]
mod tests {
use crate::configuration::{Configuration, RuleSelection};
use ruff::codes::{Flake8Copyright, Pycodestyle};
use ruff::codes::{Flake8Copyright, Pycodestyle, Refurb};
use ruff::registry::{Linter, Rule, RuleSet};
use ruff::settings::types::PreviewMode;
use ruff::RuleSelector;
@@ -814,6 +851,8 @@ mod tests {
Rule::QuadraticListSummation,
];
const PREVIEW_RULES: &[Rule] = &[Rule::SliceCopy];
#[allow(clippy::needless_pass_by_value)]
fn resolve_rules(
selections: impl IntoIterator<Item = RuleSelection>,
@@ -1099,6 +1138,29 @@ mod tests {
assert_eq!(actual, expected);
}
#[test]
fn select_rule_preview() {
let actual = resolve_rules(
[RuleSelection {
select: Some(vec![Refurb::_145.into()]),
..RuleSelection::default()
}],
Some(PreviewMode::Disabled),
);
let expected = RuleSet::empty();
assert_eq!(actual, expected);
let actual = resolve_rules(
[RuleSelection {
select: Some(vec![Refurb::_145.into()]),
..RuleSelection::default()
}],
Some(PreviewMode::Enabled),
);
let expected = RuleSet::from_rule(Rule::SliceCopy);
assert_eq!(actual, expected);
}
#[test]
fn select_preview() {
let actual = resolve_rules(
@@ -1118,7 +1180,9 @@ mod tests {
}],
Some(PreviewMode::Enabled),
);
let expected = RuleSet::from_rules(NURSERY_RULES);
let expected =
RuleSet::from_rules(NURSERY_RULES).union(&RuleSet::from_rules(PREVIEW_RULES));
assert_eq!(actual, expected);
}

2
ruff.schema.json generated
View File

@@ -2079,6 +2079,8 @@
"FURB13",
"FURB131",
"FURB132",
"FURB14",
"FURB145",
"G",
"G0",
"G00",

View File

@@ -1 +0,0 @@
1.72

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "1.72"

File diff suppressed because it is too large Load Diff

View File

@@ -6,70 +6,31 @@ authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
[tool.poetry.dependencies]
python = ">=3.10,<3.12"
autoflake = "^2.0.0"
flake8 = "^6.0.0"
pycodestyle = "^2.10.0"
pyflakes = "^3.0.1"
pylint = "^2.15.10"
black = "^22.12.0"
isort = "^5.11.4"
flake8-2020 = { version = "*", optional = true }
flake8-annotations = { version = "*", optional = true }
flake8-bandit = { version = "*", optional = true }
flake8-blind-except = { version = "*", optional = true }
# flake8-boolean-trap = { version = "*", optional = true }
flake8-bugbear = { version = "*", optional = true }
flake8-builtins = { version = "*", optional = true }
flake8-commas = { version = "*", optional = true }
flake8-comprehensions = { version = "*", optional = true }
flake8-datetimez = { version = "*", optional = true }
flake8-debugger = { version = "*", optional = true }
flake8-docstrings = { version = "*", optional = true }
# flake8-eradicate = { version = "*", optional = true }
flake8-errmsg = { version = "*", optional = true }
flake8-implicit-str-concat = { version = "*", optional = true }
# flake8-import-conventions = { version = "*", optional = true }
flake8-isort = { version = "*", optional = true }
flake8-pie = { version = "*", optional = true }
flake8-print = { version = "*", optional = true }
flake8-quotes = { version = "*", optional = true }
flake8-return = { version = "*", optional = true }
flake8-simplify = { version = "*", optional = true }
flake8-super = { version = "*", optional = true }
flake8-tidy-imports = { version = "*", optional = true }
pandas-vet = { version = "*", optional = true }
pep8-naming = { version = "*", optional = true }
autoflake = "*"
autopep8 = "*"
black = "*"
flake8 = "*"
isort = "*"
pycodestyle = "*"
pyflakes = "*"
pylint = "*"
yapf = "*"
[tool.poetry.dev-dependencies]
[tool.poetry.extras]
plugins = [
"flake8-2020",
"flake8-annotations",
"flake8-bandit",
"flake8-blind-except",
# "flake8-boolean-trap",
"flake8-bugbear",
"flake8-builtins",
"flake8-commas",
"flake8-comprehensions",
"flake8-datetimez",
"flake8-debugger",
"flake8-docstrings",
# "flake8-eradicate",
"flake8-errmsg",
"flake8-implicit-str-concat",
# "flake8-import-conventions",
"flake8-isort",
"flake8-pie",
"flake8-print",
"flake8-quotes",
"flake8-return",
"flake8-simplify",
"flake8-super",
"flake8-tidy-imports",
"pandas-vet",
"pep8-naming",
formatter = [
"black",
"yapf",
"autopep8",
]
linter = [
"autoflake",
"flake8",
"pycodestyle",
"pyflakes",
"pylint",
"isort",
]
[build-system]

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env sh
###
# Benchmark the Ruff formatter's performance against a variety of similar tools.
#
# Expects to be run from the repo root after invoking `cargo build --release`,
# in an environment with access to `black`, `autopep8`, and `yapf` (most recently:
# `black` v23.9.1, `autopep8` v2.0.4, and `yapf` v0.40.1).
#
# Example usage:
#
# ./scripts/benchmarks/run_formatter.sh ~/workspace/zulip
###
TARGET_DIR=${1}
# In each case, ensure that we format the code in-place before invoking a given tool. This ensures
# a fair comparison across tools, since every tool is then running on a repository that already
# matches that tool's desired formatting.
#
# For example, if we're benchmarking Black's preview style, we first run `black --preview` over the
# target directory, thus ensuring that we're benchmarking preview style against a codebase that
# already conforms to it. The same goes for yapf, autoepp8, etc.
# Benchmark 1: Write to disk.
hyperfine --ignore-failure \
--prepare "./target/release/ruff format ${TARGET_DIR}" \
"./target/release/ruff format ${TARGET_DIR}" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
"autopep8 ${TARGET_DIR} --recursive --in-place" \
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
"yapf ${TARGET_DIR} --parallel --recursive --in-place"
# Benchmark 2: Write to disk, but only use one thread.
hyperfine --ignore-failure \
--prepare "./target/release/ruff format ${TARGET_DIR}" \
"RAYON_NUM_THREADS=1 ./target/release/ruff format ${TARGET_DIR}" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --safe" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --fast" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --safe --preview" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --fast --preview" \
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
"autopep8 ${TARGET_DIR} --in-place --recursive --jobs=1" \
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
"yapf ${TARGET_DIR} --recursive --in-place"
# Benchmark 3: Check formatting, but don't write to disk.
hyperfine --ignore-failure \
--prepare "./target/release/ruff format ${TARGET_DIR}" \
"./target/release/ruff format ${TARGET_DIR} --check" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --safe" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --fast" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --safe --preview" \
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --fast --preview" \
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
"autopep8 ${TARGET_DIR} --recursive --diff" \
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
"yapf ${TARGET_DIR} --parallel --recursive --quiet"