Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e7b92991b | ||
|
|
25d4ddaa60 | ||
|
|
63a5a12a41 | ||
|
|
c32f943d86 | ||
|
|
d211074f59 | ||
|
|
4ffd4ed61f | ||
|
|
a4dd1e5fad | ||
|
|
be3307e9a6 | ||
|
|
317d3dd612 | ||
|
|
f5e850745c | ||
|
|
a7d1f7e1ec | ||
|
|
88c8b47326 | ||
|
|
133a745de1 | ||
|
|
6983d96d27 | ||
|
|
3c3d9ab173 | ||
|
|
ff9fb0da54 | ||
|
|
9792b1551b | ||
|
|
d1c67f91bd | ||
|
|
dbd84c947b | ||
|
|
c2ec5f0bc9 | ||
|
|
31032f4f70 | ||
|
|
f55b724254 | ||
|
|
fd07a12a52 | ||
|
|
1ee73bdedf | ||
|
|
23b55aea30 | ||
|
|
e36afc3324 | ||
|
|
8304c41714 | ||
|
|
6f31e9c00e | ||
|
|
a6cc56fd98 | ||
|
|
0236e0751c | ||
|
|
2d0769e324 | ||
|
|
80473c3f5c | ||
|
|
4d7f90e045 | ||
|
|
75bd95e58c | ||
|
|
3bbdfee69f | ||
|
|
3127c79b29 | ||
|
|
c91cc29d6d | ||
|
|
8b665f40c8 | ||
|
|
84979f9673 | ||
|
|
c3dabc1933 | ||
|
|
2e81b9c391 | ||
|
|
9feb86caa4 | ||
|
|
2587aef1ea | ||
|
|
7f4ea6690d | ||
|
|
2f32a57cf4 | ||
|
|
802616aac0 | ||
|
|
7100e12cc3 | ||
|
|
5a95b25aa8 | ||
|
|
833814384a | ||
|
|
39e45aa06f | ||
|
|
92baa3591d | ||
|
|
0e96482085 | ||
|
|
b9bff6f5d1 | ||
|
|
2401e91ab9 | ||
|
|
6199590072 | ||
|
|
08519e22e4 | ||
|
|
c704674190 | ||
|
|
2db96067aa | ||
|
|
8cd09c88d3 | ||
|
|
ce9bd19885 | ||
|
|
764304faf1 | ||
|
|
6fc35dd075 | ||
|
|
c0710a1dd4 | ||
|
|
2c2ebf952a | ||
|
|
d6a4283003 | ||
|
|
95702e408f | ||
|
|
bcaac9693b | ||
|
|
d6f59e4131 | ||
|
|
66e6388776 | ||
|
|
8472a7e50f | ||
|
|
7586091437 | ||
|
|
4e07a65c15 | ||
|
|
00fd324c6f | ||
|
|
2414f23abb | ||
|
|
e0f9dbcd10 | ||
|
|
6ddb0fa950 | ||
|
|
df807ff912 | ||
|
|
f6d6200aae | ||
|
|
fa556d1c74 | ||
|
|
860ffb9549 | ||
|
|
90ebea86a4 | ||
|
|
7a5f98835a | ||
|
|
348b649b5c | ||
|
|
ae41d6f30a | ||
|
|
b1072049bf | ||
|
|
bc49492085 | ||
|
|
c8464c3a90 | ||
|
|
f158536fbb | ||
|
|
1dd264b019 | ||
|
|
a525f09008 | ||
|
|
256b98ab9a | ||
|
|
962472da96 | ||
|
|
a00c445580 | ||
|
|
0e58433715 |
43
.github/workflows/ci.yaml
vendored
43
.github/workflows/ci.yaml
vendored
@@ -225,12 +225,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install nightly Rust toolchain"
|
||||
# Only pinned to make caching work, update freely
|
||||
run: rustup toolchain install nightly-2023-06-08
|
||||
run: rustup toolchain install nightly-2023-10-15
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install cargo-udeps"
|
||||
uses: taiki-e/install-action@cargo-udeps
|
||||
- name: "Run cargo-udeps"
|
||||
run: cargo +nightly-2023-06-08 udeps
|
||||
run: cargo +nightly-2023-10-15 udeps
|
||||
|
||||
python-package:
|
||||
name: "python package"
|
||||
@@ -337,6 +337,45 @@ jobs:
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/progress_projects
|
||||
|
||||
check-ruff-lsp:
|
||||
name: "test ruff-lsp"
|
||||
runs-on: ubuntu-latest
|
||||
needs: cargo-test
|
||||
steps:
|
||||
- uses: extractions/setup-just@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
name: "Download ruff-lsp source"
|
||||
with:
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- name: Install ruff-lsp dependencies
|
||||
run: |
|
||||
just install
|
||||
|
||||
- name: Run ruff-lsp tests
|
||||
run: |
|
||||
# Setup development binary
|
||||
pip uninstall --yes ruff
|
||||
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
|
||||
ruff version
|
||||
|
||||
just test
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -24,6 +24,10 @@ repos:
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black.md
|
||||
)$
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.33.0
|
||||
|
||||
170
CHANGELOG.md
170
CHANGELOG.md
@@ -1,53 +1,163 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.3
|
||||
|
||||
This release includes a variety of improvements to the Ruff formatter, removing several known and
|
||||
unintentional deviations from Black.
|
||||
|
||||
### Formatter
|
||||
|
||||
- Avoid space around pow for `None`, `True` and `False` ([#8189](https://github.com/astral-sh/ruff/pull/8189))
|
||||
- Avoid sorting all paths in the format command ([#8181](https://github.com/astral-sh/ruff/pull/8181))
|
||||
- Insert necessary blank line between class and leading comments ([#8224](https://github.com/astral-sh/ruff/pull/8224))
|
||||
- Avoid introducing new parentheses in annotated assignments ([#8233](https://github.com/astral-sh/ruff/pull/8233))
|
||||
- Refine the warnings about incompatible linter options ([#8196](https://github.com/astral-sh/ruff/pull/8196))
|
||||
- Add test and basic implementation for formatter preview mode ([#8044](https://github.com/astral-sh/ruff/pull/8044))
|
||||
- Refine warning about incompatible `isort` settings ([#8192](https://github.com/astral-sh/ruff/pull/8192))
|
||||
- Only omit optional parentheses for starting or ending with parentheses ([#8238](https://github.com/astral-sh/ruff/pull/8238))
|
||||
- Use source type to determine parser mode for formatting ([#8205](https://github.com/astral-sh/ruff/pull/8205))
|
||||
- Don't warn about magic trailing comma when `isort.force-single-line` is true ([#8244](https://github.com/astral-sh/ruff/pull/8244))
|
||||
- Use `SourceKind::diff` for formatter ([#8240](https://github.com/astral-sh/ruff/pull/8240))
|
||||
- Fix `fmt:off` with trailing child comment ([#8234](https://github.com/astral-sh/ruff/pull/8234))
|
||||
- Formatter parentheses support for `IpyEscapeCommand` ([#8207](https://github.com/astral-sh/ruff/pull/8207))
|
||||
|
||||
### Linter
|
||||
|
||||
- \[`pylint`\] Add buffer methods to `bad-dunder-method-name` (`PLW3201`) exclusions ([#8190](https://github.com/astral-sh/ruff/pull/8190))
|
||||
- Match rule prefixes from `external` codes setting in `unused-noqa` ([#8177](https://github.com/astral-sh/ruff/pull/8177))
|
||||
- Use `line-length` setting for isort in lieu of `pycodestyle.max-line-length` ([#8235](https://github.com/astral-sh/ruff/pull/8235))
|
||||
- Update fix for `unnecessary-paren-on-raise-exception` to unsafe for unknown types ([#8231](https://github.com/astral-sh/ruff/pull/8231))
|
||||
- Correct quick fix message for `W605` ([#8255](https://github.com/astral-sh/ruff/pull/8255))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix typo in max-doc-length documentation ([#8201](https://github.com/astral-sh/ruff/pull/8201))
|
||||
- Improve documentation around linter-formatter conflicts ([#8257](https://github.com/astral-sh/ruff/pull/8257))
|
||||
- Fix link to error suppression documentation in `unused-noqa` ([#8172](https://github.com/astral-sh/ruff/pull/8172))
|
||||
- Add `external` option to `unused-noqa` documentation ([#8171](https://github.com/astral-sh/ruff/pull/8171))
|
||||
- Add title attribute to icons ([#8060](https://github.com/astral-sh/ruff/pull/8060))
|
||||
- Clarify unsafe case in RSE102 ([#8256](https://github.com/astral-sh/ruff/pull/8256))
|
||||
- Fix skipping formatting examples ([#8210](https://github.com/astral-sh/ruff/pull/8210))
|
||||
- docs: fix name of `magic-trailing-comma` option in README ([#8200](https://github.com/astral-sh/ruff/pull/8200))
|
||||
- Add note about scope of rule changing in versioning policy ([#8169](https://github.com/astral-sh/ruff/pull/8169))
|
||||
- Document: Fix default lint rules ([#8218](https://github.com/astral-sh/ruff/pull/8218))
|
||||
- Fix a wrong setting in configuration.md ([#8186](https://github.com/astral-sh/ruff/pull/8186))
|
||||
- Fix misspelled TOML headers in the tutorial ([#8209](https://github.com/astral-sh/ruff/pull/8209))
|
||||
|
||||
## 0.1.2
|
||||
|
||||
This release includes the Beta version of the Ruff formatter — an extremely fast, Black-compatible Python formatter.
|
||||
Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blog/the-ruff-formatter) and [read the docs](https://docs.astral.sh/ruff/formatter/).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pylint`\] Implement `non-ascii-module-import` (`C2403`) ([#8056](https://github.com/astral-sh/ruff/pull/8056))
|
||||
- \[`pylint`\] implement `non-ascii-name` (`C2401`) ([#8038](https://github.com/astral-sh/ruff/pull/8038))
|
||||
- \[`pylint`\] Implement unnecessary-lambda (W0108) ([#7953](https://github.com/astral-sh/ruff/pull/7953))
|
||||
- \[`refurb`\] Implement `read-whole-file` (`FURB101`) ([#7682](https://github.com/astral-sh/ruff/pull/7682))
|
||||
- Add fix for `E223`, `E224`, and `E242` ([#8143](https://github.com/astral-sh/ruff/pull/8143))
|
||||
- Add fix for `E225`, `E226`, `E227`, and `E228` ([#8136](https://github.com/astral-sh/ruff/pull/8136))
|
||||
- Add fix for `E252` ([#8142](https://github.com/astral-sh/ruff/pull/8142))
|
||||
- Add fix for `E261` ([#8114](https://github.com/astral-sh/ruff/pull/8114))
|
||||
- Add fix for `E273` and `E274` ([#8144](https://github.com/astral-sh/ruff/pull/8144))
|
||||
- Add fix for `E275` ([#8133](https://github.com/astral-sh/ruff/pull/8133))
|
||||
- Update `SIM401` to catch ternary operations ([#7415](https://github.com/astral-sh/ruff/pull/7415))
|
||||
- Update `E721` to allow `is` and `is` not for direct type comparisons ([#7905](https://github.com/astral-sh/ruff/pull/7905))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113))
|
||||
- Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140))
|
||||
- Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097))
|
||||
- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Change `line-ending` default to `auto` ([#8057](https://github.com/astral-sh/ruff/pull/8057))
|
||||
- Respect parenthesized generators in `has_own_parentheses` ([#8100](https://github.com/astral-sh/ruff/pull/8100))
|
||||
- Add caching to formatter ([#8089](https://github.com/astral-sh/ruff/pull/8089))
|
||||
- Remove `--line-length` option from `format` command ([#8131](https://github.com/astral-sh/ruff/pull/8131))
|
||||
- Add formatter to `line-length` documentation ([#8150](https://github.com/astral-sh/ruff/pull/8150))
|
||||
- Warn about incompatible formatter options ([#8088](https://github.com/astral-sh/ruff/pull/8088))
|
||||
- Fix range of unparenthesized tuple subject in match statement ([#8101](https://github.com/astral-sh/ruff/pull/8101))
|
||||
- Remove experimental formatter warning ([#8148](https://github.com/astral-sh/ruff/pull/8148))
|
||||
- Don't move type param opening parenthesis comment ([#8163](https://github.com/astral-sh/ruff/pull/8163))
|
||||
- Update versions in format benchmark script ([#8110](https://github.com/astral-sh/ruff/pull/8110))
|
||||
- Avoid loading files for cached format results ([#8134](https://github.com/astral-sh/ruff/pull/8134))
|
||||
|
||||
### CLI
|
||||
|
||||
- Show the `ruff format` command in help menus ([#8167](https://github.com/astral-sh/ruff/pull/8167))
|
||||
- Add `ruff version` command with long version display ([#8034](https://github.com/astral-sh/ruff/pull/8034))
|
||||
|
||||
### Configuration
|
||||
|
||||
- New `pycodestyle.max-line-length` option ([#8039](https://github.com/astral-sh/ruff/pull/8039))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Detect `sys.version_info` slices in `outdated-version-block` ([#8112](https://github.com/astral-sh/ruff/pull/8112))
|
||||
- Avoid if-else simplification for `TYPE_CHECKING` blocks ([#8072](https://github.com/astral-sh/ruff/pull/8072))
|
||||
- Avoid false-positive print separator diagnostic with starred argument ([#8079](https://github.com/astral-sh/ruff/pull/8079))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix message for `too-many-arguments` lint ([#8092](https://github.com/astral-sh/ruff/pull/8092))
|
||||
- Fix `extend-unsafe-fixes` and `extend-safe-fixes` example ([#8139](https://github.com/astral-sh/ruff/pull/8139))
|
||||
- Add links to `flake8-import-conventions` options ([#8115](https://github.com/astral-sh/ruff/pull/8115))
|
||||
- Rework the documentation to incorporate the Ruff formatter ([#7732](https://github.com/astral-sh/ruff/pull/7732))
|
||||
- Fix `Options` JSON schema description ([#8081](https://github.com/astral-sh/ruff/pull/8081))
|
||||
- Fix typo (`pytext` -> `pytest`) ([#8117](https://github.com/astral-sh/ruff/pull/8117))
|
||||
- Improve `magic-value-comparison` example in docs ([#8111](https://github.com/astral-sh/ruff/pull/8111))
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Add unsafe fix for `escape-sequence-in-docstring` (`D301`) (#7970)
|
||||
- Add unsafe fix for `escape-sequence-in-docstring` (`D301`) ([#7970](https://github.com/astral-sh/ruff/pull/7970))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Respect `#(deprecated)` attribute in configuration options (#8035)
|
||||
- Add `[format|lint].exclude` options (#8000)
|
||||
- Respect `tab-size` setting in formatter (#8006)
|
||||
- Add `lint.preview` (#8002)
|
||||
- Respect `#(deprecated)` attribute in configuration options ([#8035](https://github.com/astral-sh/ruff/pull/8035))
|
||||
- Add `[format|lint].exclude` options ([#8000](https://github.com/astral-sh/ruff/pull/8000))
|
||||
- Respect `tab-size` setting in formatter ([#8006](https://github.com/astral-sh/ruff/pull/8006))
|
||||
- Add `lint.preview` ([#8002](https://github.com/astral-sh/ruff/pull/8002))
|
||||
|
||||
## Preview features
|
||||
### Preview features
|
||||
|
||||
- \[`pylint`\] Implement `literal-membership` (`PLR6201`) (#7973)
|
||||
- \[`pylint`\] Implement `too-many-boolean-expressions` (`PLR0916`) (#7975)
|
||||
- \[`pylint`\] Implement `misplaced-bare-raise` (`E0704`) (#7961)
|
||||
- \[`pylint`\] Implement `global-at-module-level` (`W0604`) (#8058)
|
||||
- \[`pylint`\] Implement `unspecified-encoding` (`PLW1514`) (#7939)
|
||||
- Add fix for `triple-single-quotes` (`D300`) (#7967)
|
||||
- \[`pylint`\] Implement `literal-membership` (`PLR6201`) ([#7973](https://github.com/astral-sh/ruff/pull/7973))
|
||||
- \[`pylint`\] Implement `too-many-boolean-expressions` (`PLR0916`) ([#7975](https://github.com/astral-sh/ruff/pull/7975))
|
||||
- \[`pylint`\] Implement `misplaced-bare-raise` (`E0704`) ([#7961](https://github.com/astral-sh/ruff/pull/7961))
|
||||
- \[`pylint`\] Implement `global-at-module-level` (`W0604`) ([#8058](https://github.com/astral-sh/ruff/pull/8058))
|
||||
- \[`pylint`\] Implement `unspecified-encoding` (`PLW1514`) ([#7939](https://github.com/astral-sh/ruff/pull/7939))
|
||||
- Add fix for `triple-single-quotes` (`D300`) ([#7967](https://github.com/astral-sh/ruff/pull/7967))
|
||||
|
||||
### Formatter
|
||||
|
||||
- New code style badge for `ruff format` (#7878)
|
||||
- Fix comments outside expression parentheses (#7873)
|
||||
- Add `--target-version` to `ruff format` (#8055)
|
||||
- Skip over parentheses when detecting `in` keyword (#8054)
|
||||
- Add `--diff` option to `ruff format` (#7937)
|
||||
- Insert newline after nested function or class statements (#7946)
|
||||
- Use `pass` over ellipsis in non-function/class contexts (#8049)
|
||||
- New code style badge for `ruff format` ([#7878](https://github.com/astral-sh/ruff/pull/7878))
|
||||
- Fix comments outside expression parentheses ([#7873](https://github.com/astral-sh/ruff/pull/7873))
|
||||
- Add `--target-version` to `ruff format` ([#8055](https://github.com/astral-sh/ruff/pull/8055))
|
||||
- Skip over parentheses when detecting `in` keyword ([#8054](https://github.com/astral-sh/ruff/pull/8054))
|
||||
- Add `--diff` option to `ruff format` ([#7937](https://github.com/astral-sh/ruff/pull/7937))
|
||||
- Insert newline after nested function or class statements ([#7946](https://github.com/astral-sh/ruff/pull/7946))
|
||||
- Use `pass` over ellipsis in non-function/class contexts ([#8049](https://github.com/astral-sh/ruff/pull/8049))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Lazily evaluate all PEP 695 type alias values (#8033)
|
||||
- Avoid failed assertion when showing fixes from stdin (#8029)
|
||||
- Avoid flagging HTTP and HTTPS literals in urllib-open (#8046)
|
||||
- Avoid flagging `bad-dunder-method-name` for `_` (#8015)
|
||||
- Remove Python 2-only methods from `URLOpen` audit (#8047)
|
||||
- Use set bracket replacement for `iteration-over-set` to preserve whitespace and comments (#8001)
|
||||
- Lazily evaluate all PEP 695 type alias values ([#8033](https://github.com/astral-sh/ruff/pull/8033))
|
||||
- Avoid failed assertion when showing fixes from stdin ([#8029](https://github.com/astral-sh/ruff/pull/8029))
|
||||
- Avoid flagging HTTP and HTTPS literals in urllib-open ([#8046](https://github.com/astral-sh/ruff/pull/8046))
|
||||
- Avoid flagging `bad-dunder-method-name` for `_` ([#8015](https://github.com/astral-sh/ruff/pull/8015))
|
||||
- Remove Python 2-only methods from `URLOpen` audit ([#8047](https://github.com/astral-sh/ruff/pull/8047))
|
||||
- Use set bracket replacement for `iteration-over-set` to preserve whitespace and comments ([#8001](https://github.com/astral-sh/ruff/pull/8001))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update tutorial to match revised Ruff defaults (#8066)
|
||||
- Update rule `B005` docs (#8028)
|
||||
- Update GitHub actions example in docs to use `--output-format` (#8014)
|
||||
- Document `lint.preview` and `format.preview` (#8032)
|
||||
- Clarify that new rules should be added to `RuleGroup::Preview`. (#7989)
|
||||
- Update tutorial to match revised Ruff defaults ([#8066](https://github.com/astral-sh/ruff/pull/8066))
|
||||
- Update rule `B005` docs ([#8028](https://github.com/astral-sh/ruff/pull/8028))
|
||||
- Update GitHub actions example in docs to use `--output-format` ([#8014](https://github.com/astral-sh/ruff/pull/8014))
|
||||
- Document `lint.preview` and `format.preview` ([#8032](https://github.com/astral-sh/ruff/pull/8032))
|
||||
- Clarify that new rules should be added to `RuleGroup::Preview`. ([#7989](https://github.com/astral-sh/ruff/pull/7989))
|
||||
|
||||
## 0.1.0
|
||||
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -407,9 +407,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b3238416c10f19985b52a937c5b3efc3ed7efe8f7ae263d2aab29a09bca9f57"
|
||||
checksum = "d680ccd1eedd2dd7c7a3649a78c7d06e0f16b191b30d81cc58e7bc906488d344"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"libc",
|
||||
@@ -418,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fecc18f65b942d2b033545bb3bd8430a23eecbbe53fad3b1342fb0e5514bca7b"
|
||||
checksum = "58b48b6c8e890d7d4ad0ed85e9ab4949bf7023198c006000ef6338ba84cf5b71"
|
||||
dependencies = [
|
||||
"codspeed",
|
||||
"colored",
|
||||
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2051,7 +2051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2188,7 +2188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.1",
|
||||
@@ -2438,7 +2438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2735,9 +2735,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
|
||||
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
@@ -2745,9 +2745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c"
|
||||
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -2839,9 +2839,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.2"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
|
||||
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -2970,18 +2970,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3104,9 +3104,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
|
||||
@@ -41,12 +41,12 @@ similar = { version = "2.3.0", features = ["inline"] }
|
||||
smallvec = { version = "1.11.1" }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.25.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.25.2" }
|
||||
strum_macros = { version = "0.25.3" }
|
||||
syn = { version = "2.0.38" }
|
||||
test-case = { version = "3.2.1" }
|
||||
thiserror = { version = "1.0.49" }
|
||||
thiserror = { version = "1.0.50" }
|
||||
toml = { version = "0.7.8" }
|
||||
tracing = { version = "0.1.39" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.4" }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
|
||||
100
README.md
100
README.md
@@ -10,7 +10,7 @@
|
||||
|
||||
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
|
||||
|
||||
An extremely fast Python linter, written in Rust.
|
||||
An extremely fast Python linter and code formatter, written in Rust.
|
||||
|
||||
<p align="center">
|
||||
<picture align="center">
|
||||
@@ -24,16 +24,15 @@ An extremely fast Python linter, written in Rust.
|
||||
<i>Linting the CPython codebase from scratch.</i>
|
||||
</p>
|
||||
|
||||
- ⚡️ 10-100x faster than existing linters
|
||||
- ⚡️ 10-100x faster than existing linters (like Flake8) and formatters (like Black)
|
||||
- 🐍 Installable via `pip`
|
||||
- 🛠️ `pyproject.toml` support
|
||||
- 🤝 Python 3.12 compatibility
|
||||
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8), isort, and Black
|
||||
- 📦 Built-in caching, to avoid re-analyzing unchanged files
|
||||
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
|
||||
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/)
|
||||
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
|
||||
built-in Flake8 rule set
|
||||
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
|
||||
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
|
||||
of popular Flake8 plugins, like flake8-bugbear
|
||||
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editor-integrations/) for
|
||||
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
|
||||
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#pyprojecttoml-discovery)
|
||||
@@ -42,10 +41,10 @@ Ruff aims to be orders of magnitude faster than alternative tools while integrat
|
||||
functionality behind a single, common interface.
|
||||
|
||||
Ruff can be used to replace [Flake8](https://pypi.org/project/flake8/) (plus dozens of plugins),
|
||||
[isort](https://pypi.org/project/isort/), [pydocstyle](https://pypi.org/project/pydocstyle/),
|
||||
[yesqa](https://github.com/asottile/yesqa), [eradicate](https://pypi.org/project/eradicate/),
|
||||
[pyupgrade](https://pypi.org/project/pyupgrade/), and [autoflake](https://pypi.org/project/autoflake/),
|
||||
all while executing tens or hundreds of times faster than any individual tool.
|
||||
[Black](https://github.com/psf/black), [isort](https://pypi.org/project/isort/),
|
||||
[pydocstyle](https://pypi.org/project/pydocstyle/), [pyupgrade](https://pypi.org/project/pyupgrade/),
|
||||
[autoflake](https://pypi.org/project/autoflake/), and more, all while executing tens or hundreds of
|
||||
times faster than any individual tool.
|
||||
|
||||
Ruff is extremely actively developed and used in major open-source projects like:
|
||||
|
||||
@@ -126,23 +125,38 @@ and with [a variety of other package managers](https://docs.astral.sh/ruff/insta
|
||||
|
||||
### Usage
|
||||
|
||||
To run Ruff, try any of the following:
|
||||
To run Ruff as a linter, try any of the following:
|
||||
|
||||
```shell
|
||||
ruff check . # Lint all files in the current directory (and any subdirectories)
|
||||
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories)
|
||||
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`
|
||||
ruff check path/to/code/to/file.py # Lint `file.py`
|
||||
ruff check . # Lint all files in the current directory (and any subdirectories).
|
||||
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories).
|
||||
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`.
|
||||
ruff check path/to/code/to/file.py # Lint `file.py`.
|
||||
ruff check @arguments.txt # Lint using an input file, treating its contents as newline-delimited command-line arguments.
|
||||
```
|
||||
|
||||
Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
Or, to run Ruff as a formatter:
|
||||
|
||||
```shell
|
||||
ruff format . # Format all files in the current directory (and any subdirectories).
|
||||
ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories).
|
||||
ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`.
|
||||
ruff format path/to/code/to/file.py # Format `file.py`.
|
||||
ruff format @arguments.txt # Format using an input file, treating its contents as newline-delimited command-line arguments.
|
||||
```
|
||||
|
||||
Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit):
|
||||
|
||||
```yaml
|
||||
# Run the Ruff linter.
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.1
|
||||
rev: v0.1.3
|
||||
hooks:
|
||||
# Run the Ruff linter.
|
||||
- id: ruff
|
||||
# Run the Ruff formatter.
|
||||
- id: ruff-format
|
||||
```
|
||||
|
||||
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or
|
||||
@@ -168,18 +182,10 @@ Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml`
|
||||
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
|
||||
for a complete list of all configuration options).
|
||||
|
||||
If left unspecified, the default configuration is equivalent to:
|
||||
If left unspecified, Ruff's default configuration is equivalent to:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
@@ -207,27 +213,46 @@ exclude = [
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.8
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
# Unlike Flake8, default to a complexity level of 10.
|
||||
max-complexity = 10
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "double"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
```
|
||||
|
||||
Some configuration options can be provided via the command-line, such as those related to
|
||||
rule enablement and disablement, file discovery, logging level, and more:
|
||||
rule enablement and disablement, file discovery, and logging level:
|
||||
|
||||
```shell
|
||||
ruff check path/to/code/ --select F401 --select F403 --quiet
|
||||
```
|
||||
|
||||
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` for more on the
|
||||
linting command.
|
||||
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
|
||||
for more on the linting and formatting commands, respectively.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -238,7 +263,7 @@ isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implement
|
||||
Rust as a first-party feature.
|
||||
|
||||
By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any
|
||||
stylistic rules that overlap with the use of a formatter, like
|
||||
stylistic rules that overlap with the use of a formatter, like `ruff format` or
|
||||
[Black](https://github.com/psf/black).
|
||||
|
||||
If you're just getting started with Ruff, **the default rule set is a great place to start**: it
|
||||
@@ -330,7 +355,7 @@ In some cases, Ruff includes a "direct" Rust port of the corresponding tool.
|
||||
We're grateful to the maintainers of these tools for their work, and for all
|
||||
the value they've provided to the Python community.
|
||||
|
||||
Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter),
|
||||
Ruff's formatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter),
|
||||
and again draws on both API and implementation details from [Rome](https://github.com/rome/tools),
|
||||
[Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black).
|
||||
|
||||
@@ -384,6 +409,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Mypy](https://github.com/python/mypy)
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [NoneBot](https://github.com/nonebot/nonebot2)
|
||||
- [ONNX](https://github.com/onnx/onnx)
|
||||
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -184,7 +184,7 @@ pub(crate) fn collect_per_file_ignores(
|
||||
for pair in pairs {
|
||||
per_file_ignores
|
||||
.entry(pair.pattern)
|
||||
.or_insert_with(Vec::new)
|
||||
.or_default()
|
||||
.push(pair.prefix);
|
||||
}
|
||||
per_file_ignores
|
||||
|
||||
@@ -37,7 +37,7 @@ serde_json.workspace = true
|
||||
url = "2.3.1"
|
||||
ureq = "2.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
codspeed-criterion-compat = { version="2.2.0", default-features = false, optional = true}
|
||||
codspeed-criterion-compat = { version="2.3.0", default-features = false, optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_linter.path = "../ruff_linter"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -66,7 +66,7 @@ wild = { version = "2" }
|
||||
assert_cmd = { version = "2.0.8" }
|
||||
# Avoid writing colored snapshots when running tests from the terminal
|
||||
colored = { workspace = true, features = ["no-color"]}
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
insta = { workspace = true, features = ["filters", "json"] }
|
||||
insta-cmd = { version = "0.4.0" }
|
||||
tempfile = "3.6.0"
|
||||
test-case = { workspace = true }
|
||||
|
||||
80
crates/ruff_cli/build.rs
Normal file
80
crates/ruff_cli/build.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
fn main() {
|
||||
// The workspace root directory is not available without walking up the tree
|
||||
// https://github.com/rust-lang/cargo/issues/3946
|
||||
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("..")
|
||||
.join("..");
|
||||
|
||||
commit_info(&workspace_root);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let target = std::env::var("TARGET").unwrap();
|
||||
println!("cargo:rustc-env=RUST_HOST_TARGET={target}");
|
||||
}
|
||||
|
||||
fn commit_info(workspace_root: &Path) {
|
||||
// If not in a git repository, do not attempt to retrieve commit information
|
||||
let git_dir = workspace_root.join(".git");
|
||||
if !git_dir.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let git_head_path = git_dir.join("HEAD");
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
git_head_path.as_path().display()
|
||||
);
|
||||
|
||||
let git_head_contents = fs::read_to_string(git_head_path);
|
||||
if let Ok(git_head_contents) = git_head_contents {
|
||||
// The contents are either a commit or a reference in the following formats
|
||||
// - "<commit>" when the head is detached
|
||||
// - "ref <ref>" when working on a branch
|
||||
// If a commit, checking if the HEAD file has changed is sufficient
|
||||
// If a ref, we need to add the head file for that ref to rebuild on commit
|
||||
let mut git_ref_parts = git_head_contents.split_whitespace();
|
||||
git_ref_parts.next();
|
||||
if let Some(git_ref) = git_ref_parts.next() {
|
||||
let git_ref_path = git_dir.join(git_ref);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
git_ref_path.as_path().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let output = match Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg("--date=short")
|
||||
.arg("--abbrev=9")
|
||||
.arg("--format=%H %h %cd %(describe)")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
_ => return,
|
||||
};
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
let mut parts = stdout.split_whitespace();
|
||||
let mut next = || parts.next().unwrap();
|
||||
println!("cargo:rustc-env=RUFF_COMMIT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RUFF_COMMIT_SHORT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RUFF_COMMIT_DATE={}", next());
|
||||
|
||||
// Describe can fail for some commits
|
||||
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
|
||||
if let Some(describe) = parts.next() {
|
||||
let mut describe_parts = describe.split('-');
|
||||
println!(
|
||||
"cargo:rustc-env=RUFF_LAST_TAG={}",
|
||||
describe_parts.next().unwrap()
|
||||
);
|
||||
// If this is the tagged commit, this component will be missing
|
||||
println!(
|
||||
"cargo:rustc-env=RUFF_LAST_TAG_DISTANCE={}",
|
||||
describe_parts.next().unwrap_or("0")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,41 @@
|
||||
"maths = (numpy.arange(100)**2).sum()\n",
|
||||
"stats= numpy.asarray([1,2,3,4]).median()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "83a0b1b8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A markdown cell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ae12f012",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# A cell with IPython escape command\n",
|
||||
"def some_function(foo, bar):\n",
|
||||
" pass\n",
|
||||
"%matplotlib inline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "10f3bbf9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"foo = %pwd\n",
|
||||
"def some_function(foo,bar,):\n",
|
||||
" # Another cell with IPython escape command\n",
|
||||
" foo = %pwd\n",
|
||||
" print(foo)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -13,6 +13,7 @@ use ruff_linter::settings::types::{
|
||||
};
|
||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::PycodestyleOptions;
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -66,9 +67,12 @@ pub enum Command {
|
||||
#[clap(alias = "--generate-shell-completion", hide = true)]
|
||||
GenerateShellCompletion { shell: clap_complete_command::Shell },
|
||||
/// Run the Ruff formatter on the given files or directories.
|
||||
#[doc(hidden)]
|
||||
#[clap(hide = true)]
|
||||
Format(FormatCommand),
|
||||
/// Display Ruff's version
|
||||
Version {
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
output_format: HelpFormat,
|
||||
},
|
||||
}
|
||||
|
||||
// The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient
|
||||
@@ -360,6 +364,14 @@ pub struct FormatCommand {
|
||||
/// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration.
|
||||
#[arg(long, conflicts_with = "isolated")]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Disable cache reads.
|
||||
#[arg(short, long, help_heading = "Miscellaneous")]
|
||||
pub no_cache: bool,
|
||||
/// Path to the cache directory.
|
||||
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
|
||||
/// Respect file exclusions via `.gitignore` and other standard ignore files.
|
||||
/// Use `--no-respect-gitignore` to disable.
|
||||
#[arg(
|
||||
@@ -389,9 +401,6 @@ pub struct FormatCommand {
|
||||
force_exclude: bool,
|
||||
#[clap(long, overrides_with("force_exclude"), hide = true)]
|
||||
no_force_exclude: bool,
|
||||
/// Set the line-length.
|
||||
#[arg(long, help_heading = "Rule configuration", hide = true)]
|
||||
pub line_length: Option<LineLength>,
|
||||
/// Ignore all configuration files.
|
||||
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
|
||||
pub isolated: bool,
|
||||
@@ -530,10 +539,10 @@ impl FormatCommand {
|
||||
config: self.config,
|
||||
files: self.files,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
},
|
||||
CliOverrides {
|
||||
line_length: self.line_length,
|
||||
respect_gitignore: resolve_bool_arg(
|
||||
self.respect_gitignore,
|
||||
self.no_respect_gitignore,
|
||||
@@ -542,6 +551,8 @@ impl FormatCommand {
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
target_version: self.target_version,
|
||||
cache_dir: self.cache_dir,
|
||||
|
||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||
..CliOverrides::default()
|
||||
},
|
||||
@@ -585,6 +596,7 @@ pub struct CheckArguments {
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct FormatArguments {
|
||||
pub check: bool,
|
||||
pub no_cache: bool,
|
||||
pub diff: bool,
|
||||
pub config: Option<PathBuf>,
|
||||
pub files: Vec<PathBuf>,
|
||||
@@ -672,8 +684,12 @@ impl ConfigurationTransformer for CliOverrides {
|
||||
if let Some(force_exclude) = &self.force_exclude {
|
||||
config.force_exclude = Some(*force_exclude);
|
||||
}
|
||||
if let Some(line_length) = &self.line_length {
|
||||
config.line_length = Some(*line_length);
|
||||
if let Some(line_length) = self.line_length {
|
||||
config.line_length = Some(line_length);
|
||||
config.lint.pycodestyle = Some(PycodestyleOptions {
|
||||
max_line_length: Some(line_length),
|
||||
..config.lint.pycodestyle.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
if let Some(preview) = &self.preview {
|
||||
config.preview = Some(*preview);
|
||||
@@ -706,7 +722,7 @@ pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgn
|
||||
for pair in pairs {
|
||||
per_file_ignores
|
||||
.entry(pair.pattern)
|
||||
.or_insert_with(Vec::new)
|
||||
.or_default()
|
||||
.push(pair.prefix);
|
||||
}
|
||||
per_file_ignores
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::{self, File};
|
||||
use std::hash::Hasher;
|
||||
use std::io::{self, BufReader, BufWriter, Write};
|
||||
@@ -8,6 +9,11 @@ use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use filetime::FileTime;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error};
|
||||
use rayon::iter::ParallelIterator;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelBridge};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -15,22 +21,50 @@ use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_diagnostics::{DiagnosticKind, Fix};
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::warn_user;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::cache;
|
||||
use crate::diagnostics::Diagnostics;
|
||||
|
||||
/// Maximum duration for which we keep a file in cache that hasn't been seen.
|
||||
const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days.
|
||||
|
||||
/// [`Path`] that is relative to the package root in [`PackageCache`].
|
||||
pub(crate) type RelativePath = Path;
|
||||
/// [`PathBuf`] that is relative to the package root in [`PackageCache`].
|
||||
pub(crate) type RelativePathBuf = PathBuf;
|
||||
|
||||
#[derive(CacheKey)]
|
||||
pub(crate) struct FileCacheKey {
|
||||
/// Timestamp when the file was last modified before the (cached) check.
|
||||
file_last_modified: FileTime,
|
||||
/// Permissions of the file before the (cached) check.
|
||||
file_permissions_mode: u32,
|
||||
}
|
||||
|
||||
impl FileCacheKey {
|
||||
pub(crate) fn from_path(path: &Path) -> io::Result<FileCacheKey> {
|
||||
// Construct a cache key for the file
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
let permissions = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
metadata.permissions().mode()
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let permissions: u32 = metadata.permissions().readonly().into();
|
||||
|
||||
Ok(FileCacheKey {
|
||||
file_last_modified: FileTime::from_last_modification_time(&metadata),
|
||||
file_permissions_mode: permissions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache.
|
||||
///
|
||||
/// `Cache` holds everything required to display the diagnostics for a single
|
||||
@@ -50,7 +84,7 @@ pub(crate) struct Cache {
|
||||
/// Files that are linted, but are not in `package.files` or are in
|
||||
/// `package.files` but are outdated. This gets merged with `package.files`
|
||||
/// when the cache is written back to disk in [`Cache::store`].
|
||||
new_files: Mutex<HashMap<RelativePathBuf, FileCache>>,
|
||||
changes: Mutex<Vec<Change>>,
|
||||
/// The "current" timestamp used as cache for the updates of
|
||||
/// [`FileCache::last_seen`]
|
||||
last_seen_cache: u64,
|
||||
@@ -65,7 +99,7 @@ impl Cache {
|
||||
///
|
||||
/// Finally `settings` is used to ensure we don't open a cache for different
|
||||
/// settings. It also defines the directory where to store the cache.
|
||||
pub(crate) fn open(package_root: PathBuf, settings: &Settings) -> Cache {
|
||||
pub(crate) fn open(package_root: PathBuf, settings: &Settings) -> Self {
|
||||
debug_assert!(package_root.is_absolute(), "package root not canonicalized");
|
||||
|
||||
let mut buf = itoa::Buffer::new();
|
||||
@@ -105,7 +139,7 @@ impl Cache {
|
||||
}
|
||||
|
||||
/// Create an empty `Cache`.
|
||||
fn empty(path: PathBuf, package_root: PathBuf) -> Cache {
|
||||
fn empty(path: PathBuf, package_root: PathBuf) -> Self {
|
||||
let package = PackageCache {
|
||||
package_root,
|
||||
files: HashMap::new(),
|
||||
@@ -114,37 +148,25 @@ impl Cache {
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn new(path: PathBuf, package: PackageCache) -> Cache {
|
||||
fn new(path: PathBuf, package: PackageCache) -> Self {
|
||||
Cache {
|
||||
path,
|
||||
package,
|
||||
new_files: Mutex::new(HashMap::new()),
|
||||
changes: Mutex::new(Vec::new()),
|
||||
// SAFETY: this will be truncated to the year ~2554 (so don't use
|
||||
// this code after that!).
|
||||
last_seen_cache: SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the cache to disk, if it has been changed.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub(crate) fn store(mut self) -> Result<()> {
|
||||
let new_files = self.new_files.into_inner().unwrap();
|
||||
if new_files.is_empty() {
|
||||
/// Applies the pending changes and persists the cache to disk, if it has been changed.
|
||||
pub(crate) fn persist(mut self) -> Result<()> {
|
||||
if !self.save() {
|
||||
// No changes made, no need to write the same cache file back to
|
||||
// disk.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Remove cached files that we haven't seen in a while.
|
||||
let now = self.last_seen_cache;
|
||||
self.package.files.retain(|_, file| {
|
||||
// SAFETY: this will be truncated to the year ~2554.
|
||||
(now - *file.last_seen.get_mut()) <= MAX_LAST_SEEN.as_millis() as u64
|
||||
});
|
||||
|
||||
// Apply any changes made and keep track of when we last saw files.
|
||||
self.package.files.extend(new_files);
|
||||
|
||||
let file = File::create(&self.path)
|
||||
.with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?;
|
||||
let writer = BufWriter::new(file);
|
||||
@@ -156,6 +178,53 @@ impl Cache {
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies the pending changes without storing the cache to disk.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub(crate) fn save(&mut self) -> bool {
|
||||
/// Maximum duration for which we keep a file in cache that hasn't been seen.
|
||||
const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days.
|
||||
|
||||
let changes = std::mem::take(self.changes.get_mut().unwrap());
|
||||
if changes.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove cached files that we haven't seen in a while.
|
||||
let now = self.last_seen_cache;
|
||||
self.package.files.retain(|_, file| {
|
||||
// SAFETY: this will be truncated to the year ~2554.
|
||||
(now - *file.last_seen.get_mut()) <= MAX_LAST_SEEN.as_millis() as u64
|
||||
});
|
||||
|
||||
// Apply any changes made and keep track of when we last saw files.
|
||||
for change in changes {
|
||||
let entry = self
|
||||
.package
|
||||
.files
|
||||
.entry(change.path)
|
||||
.and_modify(|existing| {
|
||||
if existing.key != change.new_key {
|
||||
// Reset the data if the key change.
|
||||
existing.data = FileCacheData::default();
|
||||
}
|
||||
|
||||
existing.key = change.new_key;
|
||||
existing
|
||||
.last_seen
|
||||
.store(self.last_seen_cache, Ordering::Relaxed);
|
||||
})
|
||||
.or_insert_with(|| FileCache {
|
||||
key: change.new_key,
|
||||
last_seen: AtomicU64::new(self.last_seen_cache),
|
||||
data: FileCacheData::default(),
|
||||
});
|
||||
|
||||
change.new_data.apply(&mut entry.data);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the relative path based on `path` and the package root.
|
||||
///
|
||||
/// Returns `None` if `path` is not within the package.
|
||||
@@ -169,7 +238,7 @@ impl Cache {
|
||||
///
|
||||
/// This returns `None` if `key` differs from the cached key or if the
|
||||
/// cache doesn't contain results for the file.
|
||||
pub(crate) fn get<T: CacheKey>(&self, path: &RelativePath, key: &T) -> Option<&FileCache> {
|
||||
pub(crate) fn get(&self, path: &RelativePath, key: &FileCacheKey) -> Option<&FileCache> {
|
||||
let file = self.package.files.get(path)?;
|
||||
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
@@ -185,50 +254,34 @@ impl Cache {
|
||||
Some(file)
|
||||
}
|
||||
|
||||
pub(crate) fn is_formatted(&self, path: &RelativePath, key: &FileCacheKey) -> bool {
|
||||
self.get(path, key)
|
||||
.is_some_and(|entry| entry.data.formatted)
|
||||
}
|
||||
|
||||
/// Add or update a file cache at `path` relative to the package root.
|
||||
pub(crate) fn update<T: CacheKey>(
|
||||
&self,
|
||||
path: RelativePathBuf,
|
||||
key: T,
|
||||
messages: &[Message],
|
||||
imports: &ImportMap,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
) {
|
||||
let source = if let Some(msg) = messages.first() {
|
||||
msg.file.source_text().to_owned()
|
||||
} else {
|
||||
String::new() // No messages, no need to keep the source!
|
||||
};
|
||||
|
||||
let messages = messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert!(
|
||||
msg.file == messages.first().unwrap().file,
|
||||
"message uses a different source file"
|
||||
);
|
||||
CacheMessage {
|
||||
kind: msg.kind.clone(),
|
||||
range: msg.range,
|
||||
fix: msg.fix.clone(),
|
||||
noqa_offset: msg.noqa_offset,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
fn update(&self, path: RelativePathBuf, key: &FileCacheKey, data: ChangeData) {
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
key.cache_key(&mut hasher);
|
||||
|
||||
let file = FileCache {
|
||||
key: hasher.finish(),
|
||||
last_seen: AtomicU64::new(self.last_seen_cache),
|
||||
imports: imports.clone(),
|
||||
messages,
|
||||
source,
|
||||
notebook_index: notebook_index.cloned(),
|
||||
};
|
||||
self.new_files.lock().unwrap().insert(path, file);
|
||||
self.changes.lock().unwrap().push(Change {
|
||||
path,
|
||||
new_key: hasher.finish(),
|
||||
new_data: data,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_lint(
|
||||
&self,
|
||||
path: RelativePathBuf,
|
||||
key: &FileCacheKey,
|
||||
data: LintCacheData,
|
||||
) {
|
||||
self.update(path, key, ChangeData::Lint(data));
|
||||
}
|
||||
|
||||
pub(crate) fn set_formatted(&self, path: RelativePathBuf, key: &FileCacheKey) {
|
||||
self.update(path, key, ChangeData::Formatted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,55 +307,43 @@ pub(crate) struct FileCache {
|
||||
/// Represented as the number of milliseconds since Unix epoch. This will
|
||||
/// break in 1970 + ~584 years (~2554).
|
||||
last_seen: AtomicU64,
|
||||
/// Imports made.
|
||||
imports: ImportMap,
|
||||
/// Diagnostic messages.
|
||||
messages: Vec<CacheMessage>,
|
||||
/// Source code of the file.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// This will be empty if `messages` is empty.
|
||||
source: String,
|
||||
/// Notebook index if this file is a Jupyter Notebook.
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
|
||||
data: FileCacheData,
|
||||
}
|
||||
|
||||
impl FileCache {
|
||||
/// Convert the file cache into `Diagnostics`, using `path` as file name.
|
||||
pub(crate) fn as_diagnostics(&self, path: &Path) -> Diagnostics {
|
||||
let messages = if self.messages.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*self.source).finish();
|
||||
self.messages
|
||||
.iter()
|
||||
.map(|msg| Message {
|
||||
kind: msg.kind.clone(),
|
||||
range: msg.range,
|
||||
fix: msg.fix.clone(),
|
||||
file: file.clone(),
|
||||
noqa_offset: msg.noqa_offset,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = self.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
};
|
||||
Diagnostics::new(messages, self.imports.clone(), notebook_indexes)
|
||||
pub(crate) fn to_diagnostics(&self, path: &Path) -> Option<Diagnostics> {
|
||||
self.data.lint.as_ref().map(|lint| {
|
||||
let messages = if lint.messages.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
|
||||
lint.messages
|
||||
.iter()
|
||||
.map(|msg| Message {
|
||||
kind: msg.kind.clone(),
|
||||
range: msg.range,
|
||||
fix: msg.fix.clone(),
|
||||
file: file.clone(),
|
||||
noqa_offset: msg.noqa_offset,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
};
|
||||
Diagnostics::new(messages, lint.imports.clone(), notebook_indexes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// On disk representation of a diagnostic message.
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
struct CacheMessage {
|
||||
kind: DiagnosticKind,
|
||||
/// Range into the message's [`FileCache::source`].
|
||||
range: TextRange,
|
||||
fix: Option<Fix>,
|
||||
noqa_offset: TextSize,
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct FileCacheData {
|
||||
lint: Option<LintCacheData>,
|
||||
formatted: bool,
|
||||
}
|
||||
|
||||
/// Returns a hash key based on the `package_root`, `settings` and the crate
|
||||
@@ -335,32 +376,208 @@ pub(crate) fn init(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize, PartialEq)]
|
||||
pub(crate) struct LintCacheData {
|
||||
/// Imports made.
|
||||
pub(super) imports: ImportMap,
|
||||
/// Diagnostic messages.
|
||||
pub(super) messages: Vec<CacheMessage>,
|
||||
/// Source code of the file.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// This will be empty if `messages` is empty.
|
||||
pub(super) source: String,
|
||||
/// Notebook index if this file is a Jupyter Notebook.
|
||||
pub(super) notebook_index: Option<NotebookIndex>,
|
||||
}
|
||||
|
||||
impl LintCacheData {
|
||||
pub(crate) fn from_messages(
|
||||
messages: &[Message],
|
||||
imports: ImportMap,
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
) -> Self {
|
||||
let source = if let Some(msg) = messages.first() {
|
||||
msg.file.source_text().to_owned()
|
||||
} else {
|
||||
String::new() // No messages, no need to keep the source!
|
||||
};
|
||||
|
||||
let messages = messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
msg.file,
|
||||
messages.first().unwrap().file,
|
||||
"message uses a different source file"
|
||||
);
|
||||
CacheMessage {
|
||||
kind: msg.kind.clone(),
|
||||
range: msg.range,
|
||||
fix: msg.fix.clone(),
|
||||
noqa_offset: msg.noqa_offset,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
imports,
|
||||
messages,
|
||||
source,
|
||||
notebook_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On disk representation of a diagnostic message.
|
||||
#[derive(Deserialize, Debug, Serialize, PartialEq)]
|
||||
pub(super) struct CacheMessage {
|
||||
kind: DiagnosticKind,
|
||||
/// Range into the message's [`FileCache::source`].
|
||||
range: TextRange,
|
||||
fix: Option<Fix>,
|
||||
noqa_offset: TextSize,
|
||||
}
|
||||
|
||||
pub(crate) trait PackageCaches {
|
||||
fn get(&self, package_root: &Path) -> Option<&Cache>;
|
||||
|
||||
fn persist(self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<T> PackageCaches for Option<T>
|
||||
where
|
||||
T: PackageCaches,
|
||||
{
|
||||
fn get(&self, package_root: &Path) -> Option<&Cache> {
|
||||
match self {
|
||||
None => None,
|
||||
Some(caches) => caches.get(package_root),
|
||||
}
|
||||
}
|
||||
|
||||
fn persist(self) -> Result<()> {
|
||||
match self {
|
||||
None => Ok(()),
|
||||
Some(caches) => caches.persist(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
|
||||
|
||||
impl<'a> PackageCacheMap<'a> {
|
||||
pub(crate) fn init(
|
||||
pyproject_config: &PyprojectConfig,
|
||||
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
|
||||
resolver: &Resolver,
|
||||
) -> Self {
|
||||
fn init_cache(path: &Path) {
|
||||
if let Err(e) = cache::init(path) {
|
||||
error!("Failed to initialize cache at {}: {e:?}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => {
|
||||
init_cache(&pyproject_config.settings.cache_dir);
|
||||
}
|
||||
PyprojectDiscoveryStrategy::Hierarchical => {
|
||||
for settings in
|
||||
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
|
||||
{
|
||||
init_cache(&settings.cache_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self(
|
||||
package_roots
|
||||
.iter()
|
||||
.map(|(package, package_root)| package_root.unwrap_or(package))
|
||||
.unique()
|
||||
.par_bridge()
|
||||
.map(|cache_root| {
|
||||
let settings = resolver.resolve(cache_root, pyproject_config);
|
||||
let cache = Cache::open(cache_root.to_path_buf(), settings);
|
||||
(cache_root, cache)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageCaches for PackageCacheMap<'_> {
|
||||
fn get(&self, package_root: &Path) -> Option<&Cache> {
|
||||
let cache = self.0.get(package_root);
|
||||
|
||||
if cache.is_none() {
|
||||
debug!("No cache found for {}", package_root.display());
|
||||
}
|
||||
|
||||
cache
|
||||
}
|
||||
|
||||
fn persist(self) -> Result<()> {
|
||||
self.0
|
||||
.into_par_iter()
|
||||
.try_for_each(|(_, cache)| cache.persist())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Change {
|
||||
path: PathBuf,
|
||||
new_key: u64,
|
||||
new_data: ChangeData,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ChangeData {
|
||||
Lint(LintCacheData),
|
||||
Formatted,
|
||||
}
|
||||
|
||||
impl ChangeData {
|
||||
fn apply(self, data: &mut FileCacheData) {
|
||||
match self {
|
||||
ChangeData::Lint(new_lint) => {
|
||||
data.lint = Some(new_lint);
|
||||
}
|
||||
ChangeData::Formatted => {
|
||||
data.formatted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use filetime::{set_file_mtime, FileTime};
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use std::env::temp_dir;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use anyhow::Result;
|
||||
use filetime::{set_file_mtime, FileTime};
|
||||
use itertools::Itertools;
|
||||
use test_case::test_case;
|
||||
|
||||
use ruff_cache::CACHE_DIR_NAME;
|
||||
use ruff_linter::settings::flags;
|
||||
|
||||
use crate::cache::RelativePathBuf;
|
||||
use crate::cache::{self, Cache, FileCache};
|
||||
use crate::diagnostics::{lint_path, Diagnostics};
|
||||
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_workspace::Settings;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::cache::{self, FileCache, FileCacheData, FileCacheKey};
|
||||
use crate::cache::{Cache, RelativePathBuf};
|
||||
use crate::commands::format::{format_path, FormatCommandError, FormatMode, FormatResult};
|
||||
use crate::diagnostics::{lint_path, Diagnostics};
|
||||
|
||||
#[test_case("../ruff_linter/resources/test/fixtures", "ruff_tests/cache_same_results_ruff_linter"; "ruff_linter_fixtures")]
|
||||
#[test_case("../ruff_notebook/resources/test/fixtures", "ruff_tests/cache_same_results_ruff_notebook"; "ruff_notebook_fixtures")]
|
||||
@@ -377,7 +594,7 @@ mod tests {
|
||||
|
||||
let package_root = fs::canonicalize(package_root).unwrap();
|
||||
let cache = Cache::open(package_root.clone(), &settings);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let mut parse_errors = Vec::new();
|
||||
@@ -427,7 +644,7 @@ mod tests {
|
||||
}
|
||||
assert_ne!(paths, &[] as &[std::path::PathBuf], "no files checked");
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
|
||||
let cache = Cache::open(package_root.clone(), &settings);
|
||||
assert_ne!(cache.package.files.len(), 0);
|
||||
@@ -472,21 +689,21 @@ mod tests {
|
||||
let test_cache = TestCache::new("cache_adds_file_on_lint");
|
||||
let cache = test_cache.open();
|
||||
test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
assert_eq!(
|
||||
cache.new_files.lock().unwrap().len(),
|
||||
cache.changes.lock().unwrap().len(),
|
||||
1,
|
||||
"A single new file should be added to the cache"
|
||||
);
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -497,9 +714,9 @@ mod tests {
|
||||
let cache = test_cache.open();
|
||||
test_cache.write_source_file("source_1.py", source);
|
||||
test_cache.write_source_file("source_2.py", source);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
test_cache
|
||||
@@ -509,12 +726,39 @@ mod tests {
|
||||
.lint_file_with_cache("source_2.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
assert_eq!(
|
||||
cache.new_files.lock().unwrap().len(),
|
||||
cache.changes.lock().unwrap().len(),
|
||||
2,
|
||||
"Both files should be added to the cache"
|
||||
);
|
||||
cache.persist().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_adds_files_on_format() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_adds_files_on_format");
|
||||
let cache = test_cache.open();
|
||||
test_cache.write_source_file("source_1.py", source);
|
||||
test_cache.write_source_file("source_2.py", source);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
test_cache
|
||||
.format_file_with_cache("source_1.py", &cache)
|
||||
.expect("Failed to format test file");
|
||||
test_cache
|
||||
.format_file_with_cache("source_2.py", &cache)
|
||||
.expect("Failed to format test file");
|
||||
assert_eq!(
|
||||
cache.changes.lock().unwrap().len(),
|
||||
2,
|
||||
"Both files should be added to the cache"
|
||||
);
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -524,13 +768,13 @@ mod tests {
|
||||
let test_cache = TestCache::new("cache_invalidated_on_file_modified_time");
|
||||
let cache = test_cache.open();
|
||||
let source_path = test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
let expected_diagnostics = test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
// Update the modified time of the file to a time in the future
|
||||
@@ -545,7 +789,7 @@ mod tests {
|
||||
.expect("Failed to lint test file");
|
||||
|
||||
assert_eq!(
|
||||
cache.new_files.lock().unwrap().len(),
|
||||
cache.changes.lock().unwrap().len(),
|
||||
1,
|
||||
"Cache should not be used, the file should be treated as new and added to the cache"
|
||||
);
|
||||
@@ -583,13 +827,13 @@ mod tests {
|
||||
let test_cache = TestCache::new("cache_invalidated_on_permission_change");
|
||||
let cache = test_cache.open();
|
||||
let path = test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
let expected_diagnostics = test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.unwrap();
|
||||
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
// Flip the permissions on the file
|
||||
@@ -603,7 +847,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
cache.new_files.lock().unwrap().len(),
|
||||
cache.changes.lock().unwrap().len(),
|
||||
1,
|
||||
"Cache should not be used, the file should be treated as new and added to the cache"
|
||||
);
|
||||
@@ -615,8 +859,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_removes_stale_files_on_store() {
|
||||
let test_cache = TestCache::new("cache_removes_stale_files_on_store");
|
||||
fn cache_removes_stale_files_on_persist() {
|
||||
let test_cache = TestCache::new("cache_removes_stale_files_on_persist");
|
||||
let mut cache = test_cache.open();
|
||||
|
||||
// Add a file to the cache that hasn't been linted or seen since the '70s!
|
||||
@@ -626,10 +870,7 @@ mod tests {
|
||||
FileCache {
|
||||
key: 123,
|
||||
last_seen: AtomicU64::new(123),
|
||||
imports: ImportMap::new(),
|
||||
messages: Vec::new(),
|
||||
source: String::new(),
|
||||
notebook_index: None,
|
||||
data: FileCacheData::default(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -637,34 +878,125 @@ mod tests {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
test_cache.write_source_file("new.py", source);
|
||||
let new_path_key = RelativePathBuf::from("new.py");
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
test_cache
|
||||
.lint_file_with_cache("new.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
|
||||
// Storing the cache should remove the old (`old.py`) file.
|
||||
cache.store().unwrap();
|
||||
cache.persist().unwrap();
|
||||
// So we when we open the cache again it shouldn't contain `old.py`.
|
||||
let cache = test_cache.open();
|
||||
|
||||
assert!(
|
||||
cache.package.files.keys().collect_vec() == vec![&new_path_key],
|
||||
assert_eq!(
|
||||
cache.package.files.keys().collect_vec(),
|
||||
vec![&new_path_key],
|
||||
"Only the new file should be present"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_updates_cache_entry() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("format_updates_cache_entry");
|
||||
let cache = test_cache.open();
|
||||
test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
// Cache the lint results
|
||||
test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
cache.persist().unwrap();
|
||||
|
||||
let mut cache = test_cache.open();
|
||||
|
||||
// Now lint the file
|
||||
test_cache
|
||||
.format_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to format test file");
|
||||
|
||||
cache.save();
|
||||
|
||||
assert_eq!(cache.package.files.len(), 1);
|
||||
|
||||
let Some(file_cache) = cache.get(
|
||||
Path::new("source.py"),
|
||||
&FileCacheKey::from_path(&test_cache.package_root.join("source.py")).unwrap(),
|
||||
) else {
|
||||
panic!("Cache entry for `source.py` is missing.");
|
||||
};
|
||||
|
||||
assert!(file_cache.data.lint.is_some());
|
||||
assert!(file_cache.data.formatted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_changes_invalidate_file_cache() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("file_changes_invalidate_file_cache");
|
||||
let cache = test_cache.open();
|
||||
let source_path = test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
// Cache the format and lint results
|
||||
test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
test_cache
|
||||
.format_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to format test file");
|
||||
|
||||
cache.persist().unwrap();
|
||||
|
||||
let mut cache = test_cache.open();
|
||||
assert_eq!(cache.package.files.len(), 1);
|
||||
|
||||
set_file_mtime(
|
||||
&source_path,
|
||||
FileTime::from_system_time(SystemTime::now() + std::time::Duration::from_secs(1)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
test_cache
|
||||
.format_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to format test file");
|
||||
|
||||
cache.save();
|
||||
|
||||
assert_eq!(cache.package.files.len(), 1);
|
||||
|
||||
let Some(file_cache) = cache.get(
|
||||
Path::new("source.py"),
|
||||
&FileCacheKey::from_path(&source_path).unwrap(),
|
||||
) else {
|
||||
panic!("Cache entry for `source.py` is missing.");
|
||||
};
|
||||
|
||||
assert_eq!(file_cache.data.lint, None);
|
||||
assert!(file_cache.data.formatted);
|
||||
}
|
||||
|
||||
struct TestCache {
|
||||
package_root: PathBuf,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
impl TestCache {
|
||||
fn new(name: &str) -> Self {
|
||||
fn new(test_case: &str) -> Self {
|
||||
// Build a new cache directory and clear it
|
||||
let mut test_dir = temp_dir();
|
||||
test_dir.push("ruff_tests/cache");
|
||||
test_dir.push(name);
|
||||
test_dir.push(test_case);
|
||||
|
||||
let _ = fs::remove_dir_all(&test_dir);
|
||||
|
||||
@@ -718,6 +1050,21 @@ mod tests {
|
||||
UnsafeFixes::Enabled,
|
||||
)
|
||||
}
|
||||
|
||||
fn format_file_with_cache(
|
||||
&self,
|
||||
path: &str,
|
||||
cache: &Cache,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
let file_path = self.package_root.join(path);
|
||||
format_path(
|
||||
&file_path,
|
||||
&self.settings.formatter,
|
||||
PySourceType::Python,
|
||||
FormatMode::Write,
|
||||
Some(cache),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestCache {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -7,28 +6,26 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use ignore::Error;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, warn};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use rayon::prelude::*;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{flags, LinterSettings};
|
||||
use ruff_linter::{fs, warn_user_once, IOError};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||
ResolvedFile,
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile,
|
||||
};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::cache::{self, Cache};
|
||||
use crate::cache::{Cache, PackageCacheMap, PackageCaches};
|
||||
use crate::diagnostics::Diagnostics;
|
||||
use crate::panic::catch_unwind;
|
||||
|
||||
@@ -52,28 +49,6 @@ pub(crate) fn check(
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
// Initialize the cache.
|
||||
if cache.into() {
|
||||
fn init_cache(path: &Path) {
|
||||
if let Err(e) = cache::init(path) {
|
||||
error!("Failed to initialize cache at {}: {e:?}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => {
|
||||
init_cache(&pyproject_config.settings.cache_dir);
|
||||
}
|
||||
PyprojectDiscoveryStrategy::Hierarchical => {
|
||||
for settings in
|
||||
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
|
||||
{
|
||||
init_cache(&settings.cache_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
let package_roots = resolver.package_roots(
|
||||
&paths
|
||||
@@ -85,19 +60,15 @@ pub(crate) fn check(
|
||||
);
|
||||
|
||||
// Load the caches.
|
||||
let caches = bool::from(cache).then(|| {
|
||||
package_roots
|
||||
.iter()
|
||||
.map(|(package, package_root)| package_root.unwrap_or(package))
|
||||
.unique()
|
||||
.par_bridge()
|
||||
.map(|cache_root| {
|
||||
let settings = resolver.resolve(cache_root, pyproject_config);
|
||||
let cache = Cache::open(cache_root.to_path_buf(), settings);
|
||||
(cache_root, cache)
|
||||
})
|
||||
.collect::<HashMap<&Path, Cache>>()
|
||||
});
|
||||
let caches = if bool::from(cache) {
|
||||
Some(PackageCacheMap::init(
|
||||
pyproject_config,
|
||||
&package_roots,
|
||||
&resolver,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let diagnostics_per_file = paths.par_iter().filter_map(|resolved_file| {
|
||||
@@ -122,14 +93,7 @@ pub(crate) fn check(
|
||||
}
|
||||
|
||||
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
||||
let cache = caches.as_ref().and_then(|caches| {
|
||||
if let Some(cache) = caches.get(&cache_root) {
|
||||
Some(cache)
|
||||
} else {
|
||||
debug!("No cache found for {}", cache_root.display());
|
||||
None
|
||||
}
|
||||
});
|
||||
let cache = caches.get(cache_root);
|
||||
|
||||
lint_path(
|
||||
path,
|
||||
@@ -210,11 +174,7 @@ pub(crate) fn check(
|
||||
all_diagnostics.messages.sort();
|
||||
|
||||
// Store the caches.
|
||||
if let Some(caches) = caches {
|
||||
caches
|
||||
.into_par_iter()
|
||||
.try_for_each(|(_, cache)| cache.store())?;
|
||||
}
|
||||
caches.persist()?;
|
||||
|
||||
let duration = start.elapsed();
|
||||
debug!("Checked {:?} files in: {:?}", checked_files, duration);
|
||||
@@ -266,13 +226,12 @@ mod test {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use rustc_hash::FxHashMap;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use ruff_linter::message::{Emitter, EmitterContext, TextEmitter};
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{flags, LinterSettings};
|
||||
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
@@ -8,25 +8,29 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
use rayon::iter::Either::{Left, Right};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use similar::TextDiff;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_linter::fs;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_linter::rules::flake8_quotes::settings::Quote;
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path};
|
||||
use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
|
||||
};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
use crate::ExitStatus;
|
||||
@@ -73,9 +77,36 @@ pub(crate) fn format(
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
let package_roots = resolver.package_roots(
|
||||
&paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
&pyproject_config,
|
||||
);
|
||||
|
||||
let caches = if cli.no_cache {
|
||||
None
|
||||
} else {
|
||||
// `--no-cache` doesn't respect code changes, and so is often confusing during
|
||||
// development.
|
||||
#[cfg(debug_assertions)]
|
||||
crate::warn_user!("Detected debug build without --no-cache.");
|
||||
|
||||
Some(PackageCacheMap::init(
|
||||
&pyproject_config,
|
||||
&package_roots,
|
||||
&resolver,
|
||||
))
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let (mut results, mut errors): (Vec<_>, Vec<_>) = paths
|
||||
.into_par_iter()
|
||||
let (results, mut errors): (Vec<_>, Vec<_>) = paths
|
||||
.par_iter()
|
||||
.filter_map(|entry| {
|
||||
match entry {
|
||||
Ok(resolved_file) => {
|
||||
@@ -98,41 +129,35 @@ pub(crate) fn format(
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the sources from the file.
|
||||
let unformatted = match SourceKind::from_path(path, source_type) {
|
||||
Ok(Some(source_kind)) => source_kind,
|
||||
Ok(None) => return None,
|
||||
Err(err) => {
|
||||
return Some(Err(FormatCommandError::Read(
|
||||
Some(path.to_path_buf()),
|
||||
err,
|
||||
)));
|
||||
}
|
||||
};
|
||||
let package = path
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent).copied())
|
||||
.flatten();
|
||||
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
||||
let cache = caches.get(cache_root);
|
||||
|
||||
Some(
|
||||
match catch_unwind(|| {
|
||||
format_path(
|
||||
path,
|
||||
&resolved_settings.formatter,
|
||||
&unformatted,
|
||||
source_type,
|
||||
mode,
|
||||
cache,
|
||||
)
|
||||
}) {
|
||||
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||
path: resolved_file.into_path(),
|
||||
unformatted,
|
||||
path: resolved_file.path().to_path_buf(),
|
||||
result,
|
||||
}),
|
||||
Err(error) => Err(FormatCommandError::Panic(
|
||||
Some(resolved_file.into_path()),
|
||||
Some(resolved_file.path().to_path_buf()),
|
||||
error,
|
||||
)),
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
|
||||
Err(err) => Some(Err(FormatCommandError::Ignore(err.clone()))),
|
||||
}
|
||||
})
|
||||
.partition_map(|result| match result {
|
||||
@@ -141,43 +166,30 @@ pub(crate) fn format(
|
||||
});
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Make output deterministic, at least as long as we have a path
|
||||
results.sort_unstable_by(|x, y| x.path.cmp(&y.path));
|
||||
errors.sort_by(|x, y| {
|
||||
fn get_key(error: &FormatCommandError) -> Option<&PathBuf> {
|
||||
match &error {
|
||||
FormatCommandError::Ignore(ignore) => {
|
||||
if let ignore::Error::WithPath { path, .. } = ignore {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
FormatCommandError::Panic(path, _)
|
||||
| FormatCommandError::Read(path, _)
|
||||
| FormatCommandError::Format(path, _)
|
||||
| FormatCommandError::Write(path, _) => path.as_ref(),
|
||||
}
|
||||
}
|
||||
get_key(x).cmp(&get_key(y))
|
||||
});
|
||||
|
||||
debug!(
|
||||
"Formatted {} files in {:.2?}",
|
||||
results.len() + errors.len(),
|
||||
duration
|
||||
);
|
||||
|
||||
caches.persist()?;
|
||||
|
||||
// Report on any errors.
|
||||
errors.sort_unstable_by(|a, b| a.path().cmp(&b.path()));
|
||||
|
||||
for error in &errors {
|
||||
error!("{error}");
|
||||
}
|
||||
|
||||
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
let results = FormatResults::new(results.as_slice(), mode);
|
||||
|
||||
if mode.is_diff() {
|
||||
results.write_diff(&mut stdout().lock())?;
|
||||
match mode {
|
||||
FormatMode::Write => {}
|
||||
FormatMode::Check => {
|
||||
results.write_changed(&mut stdout().lock())?;
|
||||
}
|
||||
FormatMode::Diff => {
|
||||
results.write_diff(&mut stdout().lock())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Report on the formatting changes.
|
||||
@@ -214,15 +226,37 @@ pub(crate) fn format(
|
||||
|
||||
/// Format the file at the given [`Path`].
|
||||
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
|
||||
fn format_path(
|
||||
pub(crate) fn format_path(
|
||||
path: &Path,
|
||||
settings: &FormatterSettings,
|
||||
unformatted: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
cache: Option<&Cache>,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
if let Some(cache) = cache {
|
||||
let relative_path = cache
|
||||
.relative_path(path)
|
||||
.expect("wrong package cache for file");
|
||||
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
if cache.is_formatted(relative_path, &cache_key) {
|
||||
return Ok(FormatResult::Unchanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the sources from the file.
|
||||
let unformatted = match SourceKind::from_path(path, source_type) {
|
||||
Ok(Some(source_kind)) => source_kind,
|
||||
// Non Python Jupyter notebook
|
||||
Ok(None) => return Ok(FormatResult::Skipped),
|
||||
Err(err) => {
|
||||
return Err(FormatCommandError::Read(Some(path.to_path_buf()), err));
|
||||
}
|
||||
};
|
||||
|
||||
// Format the source.
|
||||
let format_result = match format_source(unformatted, source_type, Some(path), settings)? {
|
||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::Write => {
|
||||
let mut writer = File::create(path).map_err(|err| {
|
||||
@@ -231,13 +265,38 @@ fn format_path(
|
||||
formatted
|
||||
.write(&mut writer)
|
||||
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
|
||||
|
||||
if let Some(cache) = cache {
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
let relative_path = cache
|
||||
.relative_path(path)
|
||||
.expect("wrong package cache for file");
|
||||
cache.set_formatted(relative_path.to_path_buf(), &cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
FormatResult::Formatted
|
||||
}
|
||||
FormatMode::Check => FormatResult::Formatted,
|
||||
FormatMode::Diff => FormatResult::Diff(formatted),
|
||||
FormatMode::Diff => FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
},
|
||||
},
|
||||
FormattedSource::Unchanged => FormatResult::Unchanged,
|
||||
FormattedSource::Unchanged => {
|
||||
if let Some(cache) = cache {
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
let relative_path = cache
|
||||
.relative_path(path)
|
||||
.expect("wrong package cache for file");
|
||||
cache.set_formatted(relative_path.to_path_buf(), &cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
FormatResult::Unchanged
|
||||
}
|
||||
};
|
||||
|
||||
Ok(format_result)
|
||||
}
|
||||
|
||||
@@ -354,16 +413,21 @@ pub(crate) enum FormatResult {
|
||||
/// The file was formatted.
|
||||
Formatted,
|
||||
/// The file was formatted, [`SourceKind`] contains the formatted code
|
||||
Diff(SourceKind),
|
||||
Diff {
|
||||
unformatted: SourceKind,
|
||||
formatted: SourceKind,
|
||||
},
|
||||
/// The file was unchanged, as the formatted contents matched the existing contents.
|
||||
Unchanged,
|
||||
|
||||
/// Skipped formatting because its an unsupported file format
|
||||
Skipped,
|
||||
}
|
||||
|
||||
/// The coupling of a [`FormatResult`] with the path of the file that was analyzed.
|
||||
#[derive(Debug)]
|
||||
struct FormatPathResult {
|
||||
path: PathBuf,
|
||||
unformatted: SourceKind,
|
||||
result: FormatResult,
|
||||
}
|
||||
|
||||
@@ -385,27 +449,55 @@ impl<'a> FormatResults<'a> {
|
||||
fn any_formatted(&self) -> bool {
|
||||
self.results.iter().any(|result| match result.result {
|
||||
FormatResult::Formatted | FormatResult::Diff { .. } => true,
|
||||
FormatResult::Unchanged => false,
|
||||
FormatResult::Unchanged | FormatResult::Skipped => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a diff of the formatting changes to the given writer.
|
||||
fn write_diff(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
for result in self.results {
|
||||
if let FormatResult::Diff(formatted) = &result.result {
|
||||
let text_diff =
|
||||
TextDiff::from_lines(result.unformatted.source_code(), formatted.source_code());
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
unified_diff.header(
|
||||
&fs::relativize_path(&result.path),
|
||||
&fs::relativize_path(&result.path),
|
||||
);
|
||||
unified_diff.to_writer(&mut *f)?;
|
||||
}
|
||||
for (path, unformatted, formatted) in self
|
||||
.results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
if let FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
} = &result.result
|
||||
{
|
||||
Some((result.path.as_path(), unformatted, formatted))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|(path, _, _)| *path)
|
||||
{
|
||||
unformatted.diff(formatted, Some(path), f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a list of the files that would be changed to the given writer.
|
||||
fn write_changed(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
for path in self
|
||||
.results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
if result.result.is_formatted() {
|
||||
Some(result.path.as_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sorted_unstable()
|
||||
{
|
||||
writeln!(f, "Would reformat: {}", fs::relativize_path(path).bold())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a summary of the formatting results to the given writer.
|
||||
fn write_summary(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
// Compute the number of changed and unchanged files.
|
||||
let mut changed = 0u32;
|
||||
@@ -413,20 +505,13 @@ impl<'a> FormatResults<'a> {
|
||||
for result in self.results {
|
||||
match &result.result {
|
||||
FormatResult::Formatted => {
|
||||
// If we're running in check mode, report on any files that would be formatted.
|
||||
if self.mode.is_check() {
|
||||
writeln!(
|
||||
f,
|
||||
"Would reformat: {}",
|
||||
fs::relativize_path(&result.path).bold()
|
||||
)?;
|
||||
}
|
||||
changed += 1;
|
||||
}
|
||||
FormatResult::Unchanged => unchanged += 1,
|
||||
FormatResult::Diff(_) => {
|
||||
FormatResult::Diff { .. } => {
|
||||
changed += 1;
|
||||
}
|
||||
FormatResult::Skipped => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,6 +561,26 @@ pub(crate) enum FormatCommandError {
|
||||
Read(Option<PathBuf>, SourceError),
|
||||
Format(Option<PathBuf>, FormatModuleError),
|
||||
Write(Option<PathBuf>, SourceError),
|
||||
Diff(Option<PathBuf>, io::Error),
|
||||
}
|
||||
|
||||
impl FormatCommandError {
|
||||
fn path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Ignore(err) => {
|
||||
if let ignore::Error::WithPath { path, .. } = err {
|
||||
Some(path.as_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Panic(path, _)
|
||||
| Self::Read(path, _)
|
||||
| Self::Format(path, _)
|
||||
| Self::Write(path, _)
|
||||
| Self::Diff(path, _) => path.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FormatCommandError {
|
||||
@@ -541,6 +646,24 @@ impl Display for FormatCommandError {
|
||||
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
|
||||
}
|
||||
}
|
||||
Self::Diff(path, err) => {
|
||||
if let Some(path) = path {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{} {err}",
|
||||
"Failed to generate diff for ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}{} {err}",
|
||||
"Failed to generate diff".bold(),
|
||||
":".bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::Panic(path, err) => {
|
||||
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
|
||||
|
||||
@@ -567,3 +690,120 @@ impl Display for FormatCommandError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn warn_incompatible_formatter_settings(
|
||||
pyproject_config: &PyprojectConfig,
|
||||
resolver: Option<&Resolver>,
|
||||
) {
|
||||
for setting in std::iter::once(&pyproject_config.settings)
|
||||
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
|
||||
{
|
||||
let mut incompatible_rules = Vec::new();
|
||||
|
||||
for rule in [
|
||||
// The formatter might collapse implicit string concatenation on a single line.
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
// Flags missing trailing commas when all arguments are on its own line:
|
||||
// ```python
|
||||
// def args(
|
||||
// aaaaaaaa, bbbbbbbbb, cccccccccc, ddddddddd, eeeeeeee, ffffff, gggggggggggg, hhhh
|
||||
// ):
|
||||
// pass
|
||||
// ```
|
||||
Rule::MissingTrailingComma,
|
||||
] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Rules asserting for space indentation
|
||||
if setting.formatter.indent_style.is_tab() {
|
||||
for rule in [Rule::TabIndentation, Rule::IndentWithSpaces] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rules asserting for indent-width=4
|
||||
if setting.formatter.indent_width.value() != 4 {
|
||||
for rule in [
|
||||
Rule::IndentationWithInvalidMultiple,
|
||||
Rule::IndentationWithInvalidMultipleComment,
|
||||
] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !incompatible_rules.is_empty() {
|
||||
let mut rule_names: Vec<_> = incompatible_rules
|
||||
.into_iter()
|
||||
.map(|rule| format!("`{}`", rule.noqa_code()))
|
||||
.collect();
|
||||
rule_names.sort();
|
||||
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", rule_names.join(", "));
|
||||
}
|
||||
|
||||
// Rules with different quote styles.
|
||||
if setting
|
||||
.linter
|
||||
.rules
|
||||
.any_enabled(&[Rule::BadQuotesInlineString, Rule::AvoidableEscapedQuote])
|
||||
{
|
||||
match (
|
||||
setting.linter.flake8_quotes.inline_quotes,
|
||||
setting.formatter.quote_style,
|
||||
) {
|
||||
(Quote::Double, QuoteStyle::Single) => {
|
||||
warn!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
|
||||
}
|
||||
(Quote::Single, QuoteStyle::Double) => {
|
||||
warn!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
|
||||
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
|
||||
{
|
||||
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
|
||||
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
|
||||
{
|
||||
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.rules.enabled(Rule::UnsortedImports) {
|
||||
// The formatter removes empty lines if the value is larger than 2 but always inserts a empty line after imports.
|
||||
// Two empty lines are okay because `isort` only uses this setting for top-level imports (not in nested blocks).
|
||||
if !matches!(setting.linter.isort.lines_after_imports, 1 | 2 | -1) {
|
||||
warn!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).");
|
||||
}
|
||||
|
||||
// Values larger than two get reduced to one line by the formatter if the import is in a nested block.
|
||||
if setting.linter.isort.lines_between_types > 1 {
|
||||
warn!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).");
|
||||
}
|
||||
|
||||
// isort inserts a trailing comma which the formatter preserves, but only if `skip-magic-trailing-comma` isn't false.
|
||||
// This isn't relevant when using `force-single-line`, since isort will never include a trailing comma in that case.
|
||||
if setting.formatter.magic_trailing_comma.is_ignore()
|
||||
&& !setting.linter.isort.force_single_line
|
||||
{
|
||||
if setting.linter.isort.force_wrap_aliases {
|
||||
warn!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.");
|
||||
}
|
||||
|
||||
if setting.linter.isort.split_on_trailing_comma {
|
||||
warn!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use ruff_linter::fs;
|
||||
use similar::TextDiff;
|
||||
|
||||
use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
@@ -13,7 +11,8 @@ use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::commands::format::{
|
||||
format_source, FormatCommandError, FormatMode, FormatResult, FormattedSource,
|
||||
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
||||
FormatResult, FormattedSource,
|
||||
};
|
||||
use crate::resolve::resolve;
|
||||
use crate::stdin::read_from_stdin;
|
||||
@@ -27,6 +26,9 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
overrides,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
|
||||
warn_incompatible_formatter_settings(&pyproject_config, None);
|
||||
|
||||
let mode = FormatMode::from_cli(cli);
|
||||
|
||||
if let Some(filename) = cli.stdin_filename.as_deref() {
|
||||
@@ -105,14 +107,9 @@ fn format_source_code(
|
||||
}
|
||||
FormatMode::Check => {}
|
||||
FormatMode::Diff => {
|
||||
let mut writer = stdout().lock();
|
||||
let text_diff =
|
||||
TextDiff::from_lines(source_kind.source_code(), formatted.source_code());
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
if let Some(path) = path {
|
||||
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
|
||||
}
|
||||
unified_diff.to_writer(&mut writer).unwrap();
|
||||
source_kind
|
||||
.diff(formatted, path, &mut stdout().lock())
|
||||
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
|
||||
}
|
||||
},
|
||||
FormattedSource::Unchanged => {
|
||||
|
||||
@@ -9,3 +9,4 @@ pub(crate) mod linter;
|
||||
pub(crate) mod rule;
|
||||
pub(crate) mod show_files;
|
||||
pub(crate) mod show_settings;
|
||||
pub(crate) mod version;
|
||||
|
||||
21
crates/ruff_cli/src/commands/version.rs
Normal file
21
crates/ruff_cli/src/commands/version.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::io::{self, BufWriter, Write};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::args::HelpFormat;
|
||||
|
||||
/// Display version information
|
||||
pub(crate) fn version(output_format: HelpFormat) -> Result<()> {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
let version_info = crate::version::version();
|
||||
|
||||
match output_format {
|
||||
HelpFormat::Text => {
|
||||
writeln!(stdout, "ruff {}", &version_info)?;
|
||||
}
|
||||
HelpFormat::Json => {
|
||||
serde_json::to_writer_pretty(stdout, &version_info)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,16 +3,14 @@
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::ops::{Add, AddAssign};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use colored::Colorize;
|
||||
use filetime::FileTime;
|
||||
use log::{debug, error, warn};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::cache::{Cache, FileCacheKey, LintCacheData};
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
|
||||
use ruff_linter::logging::DisplayParseError;
|
||||
@@ -23,7 +21,6 @@ use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{flags, LinterSettings};
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::{fs, IOError, SyntaxError};
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::{SourceType, TomlSourceType};
|
||||
@@ -31,33 +28,6 @@ use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::cache::Cache;
|
||||
|
||||
#[derive(CacheKey)]
|
||||
pub(crate) struct FileCacheKey {
|
||||
/// Timestamp when the file was last modified before the (cached) check.
|
||||
file_last_modified: FileTime,
|
||||
/// Permissions of the file before the (cached) check.
|
||||
file_permissions_mode: u32,
|
||||
}
|
||||
|
||||
impl FileCacheKey {
|
||||
fn from_path(path: &Path) -> io::Result<FileCacheKey> {
|
||||
// Construct a cache key for the file
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
let permissions = metadata.permissions().mode();
|
||||
#[cfg(windows)]
|
||||
let permissions: u32 = metadata.permissions().readonly().into();
|
||||
|
||||
Ok(FileCacheKey {
|
||||
file_last_modified: FileTime::from_last_modification_time(&metadata),
|
||||
file_permissions_mode: permissions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct Diagnostics {
|
||||
pub(crate) messages: Vec<Message>,
|
||||
@@ -230,9 +200,11 @@ pub(crate) fn lint_path(
|
||||
.expect("wrong package cache for file");
|
||||
|
||||
let cache_key = FileCacheKey::from_path(path).context("Failed to create cache key")?;
|
||||
|
||||
if let Some(cache) = cache.get(relative_path, &cache_key) {
|
||||
return Ok(cache.as_diagnostics(path));
|
||||
let cached_diagnostics = cache
|
||||
.get(relative_path, &cache_key)
|
||||
.and_then(|entry| entry.to_diagnostics(path));
|
||||
if let Some(diagnostics) = cached_diagnostics {
|
||||
return Ok(diagnostics);
|
||||
}
|
||||
|
||||
// Stash the file metadata for later so when we update the cache it reflects the prerun
|
||||
@@ -332,12 +304,14 @@ pub(crate) fn lint_path(
|
||||
if let Some((cache, relative_path, key)) = caching {
|
||||
// We don't cache parsing errors.
|
||||
if parse_error.is_none() {
|
||||
cache.update(
|
||||
cache.update_lint(
|
||||
relative_path.to_owned(),
|
||||
key,
|
||||
&messages,
|
||||
&imports,
|
||||
source_kind.as_ipy_notebook().map(Notebook::index),
|
||||
&key,
|
||||
LintCacheData::from_messages(
|
||||
&messages,
|
||||
imports.clone(),
|
||||
source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -27,6 +29,7 @@ mod panic;
|
||||
mod printer;
|
||||
pub mod resolve;
|
||||
mod stdin;
|
||||
mod version;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
@@ -134,6 +137,10 @@ pub fn run(
|
||||
set_up_logging(&log_level)?;
|
||||
|
||||
match command {
|
||||
Command::Version { output_format } => {
|
||||
commands::version::version(output_format)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Rule { rule, all, format } => {
|
||||
if all {
|
||||
commands::rule::rules(format)?;
|
||||
@@ -165,8 +172,6 @@ pub fn run(
|
||||
}
|
||||
|
||||
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
warn_user_once!("`ruff format` is not yet stable, and subject to change in future versions.");
|
||||
|
||||
let (cli, overrides) = args.partition();
|
||||
|
||||
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_cli/src/version.rs
|
||||
expression: version
|
||||
---
|
||||
0.0.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_cli/src/version.rs
|
||||
expression: version
|
||||
---
|
||||
0.0.0 (53b0f5d92 2023-10-19)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_cli/src/version.rs
|
||||
expression: version
|
||||
---
|
||||
0.0.0+24 (53b0f5d92 2023-10-19)
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: crates/ruff_cli/src/version.rs
|
||||
expression: version
|
||||
---
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"commit_info": {
|
||||
"short_commit_hash": "53b0f5d92",
|
||||
"commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7",
|
||||
"commit_date": "2023-10-19",
|
||||
"last_tag": "v0.0.1",
|
||||
"commits_since_last_tag": 0
|
||||
}
|
||||
}
|
||||
130
crates/ruff_cli/src/version.rs
Normal file
130
crates/ruff_cli/src/version.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! Code for representing Ruff's release version number.
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
/// Information about the git repository where Ruff was built from.
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CommitInfo {
|
||||
short_commit_hash: String,
|
||||
commit_hash: String,
|
||||
commit_date: String,
|
||||
last_tag: Option<String>,
|
||||
commits_since_last_tag: u32,
|
||||
}
|
||||
|
||||
/// Ruff's version.
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct VersionInfo {
|
||||
/// Ruff's version, such as "0.5.1"
|
||||
version: String,
|
||||
/// Information about the git commit we may have been built from.
|
||||
///
|
||||
/// `None` if not built from a git repo or if retrieval failed.
|
||||
commit_info: Option<CommitInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
/// Formatted version information: "<version>[+<commits>] (<commit> <date>)"
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.version)?;
|
||||
|
||||
if let Some(ref ci) = self.commit_info {
|
||||
if ci.commits_since_last_tag > 0 {
|
||||
write!(f, "+{}", ci.commits_since_last_tag)?;
|
||||
}
|
||||
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about Ruff's version.
|
||||
pub(crate) fn version() -> VersionInfo {
|
||||
// Environment variables are only read at compile-time
|
||||
macro_rules! option_env_str {
|
||||
($name:expr) => {
|
||||
option_env!($name).map(|s| s.to_string())
|
||||
};
|
||||
}
|
||||
|
||||
// This version is pulled from Cargo.toml and set by Cargo
|
||||
let version = option_env_str!("CARGO_PKG_VERSION").unwrap();
|
||||
|
||||
// Commit info is pulled from git and set by `build.rs`
|
||||
let commit_info = option_env_str!("RUFF_COMMIT_HASH").map(|commit_hash| CommitInfo {
|
||||
short_commit_hash: option_env_str!("RUFF_COMMIT_SHORT_HASH").unwrap(),
|
||||
commit_hash,
|
||||
commit_date: option_env_str!("RUFF_COMMIT_DATE").unwrap(),
|
||||
last_tag: option_env_str!("RUFF_LAST_TAG"),
|
||||
commits_since_last_tag: option_env_str!("RUFF_LAST_TAG_DISTANCE")
|
||||
.as_deref()
|
||||
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
|
||||
});
|
||||
|
||||
VersionInfo {
|
||||
version,
|
||||
commit_info,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::{assert_display_snapshot, assert_json_snapshot};
|
||||
|
||||
use super::{CommitInfo, VersionInfo};
|
||||
|
||||
#[test]
|
||||
fn version_formatting() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: None,
|
||||
};
|
||||
assert_display_snapshot!(version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_formatting_with_commit_info() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: Some(CommitInfo {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
|
||||
last_tag: Some("v0.0.1".to_string()),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 0,
|
||||
}),
|
||||
};
|
||||
assert_display_snapshot!(version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_formatting_with_commits_since_last_tag() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: Some(CommitInfo {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
|
||||
last_tag: Some("v0.0.1".to_string()),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 24,
|
||||
}),
|
||||
};
|
||||
assert_display_snapshot!(version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_serializable() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: Some(CommitInfo {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
|
||||
last_tag: Some("v0.0.1".to_string()),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 0,
|
||||
}),
|
||||
};
|
||||
assert_json_snapshot!(version);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ if condition:
|
||||
print('Hy "Micha"') # Should not change quotes
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ fn format_options() -> Result<()> {
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
tab-size = 8
|
||||
indent-width = 8
|
||||
line-length = 84
|
||||
|
||||
[format]
|
||||
@@ -89,7 +88,34 @@ if condition:
|
||||
print('Should change quotes')
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_line_endings() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("main.py"),
|
||||
"from test import say_hy\n\nif __name__ == \"__main__\":\n say_hy(\"dear Ruff contributor\")\n",
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("test.py"),
|
||||
"def say_hy(name: str):\r\n print(f\"Hy {name}\")\r\n",
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--diff", "--isolated"])
|
||||
.arg("."), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
2 files left unchanged
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -144,7 +170,7 @@ OTHER = "OTHER"
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--check", "--config"])
|
||||
.args(["format", "--no-cache", "--check", "--config"])
|
||||
.arg(ruff_toml.file_name().unwrap())
|
||||
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
|
||||
.arg(test_path.file_name().unwrap())
|
||||
@@ -158,7 +184,6 @@ OTHER = "OTHER"
|
||||
2 files would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -171,6 +196,7 @@ fn exclude_stdin() -> Result<()> {
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend-select = ["B", "Q"]
|
||||
ignore = ["Q000", "Q001", "Q002", "Q003"]
|
||||
|
||||
[format]
|
||||
exclude = ["generated.py"]
|
||||
@@ -191,7 +217,6 @@ if __name__ == '__main__':
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -206,6 +231,9 @@ fn format_option_inheritance() -> Result<()> {
|
||||
r#"
|
||||
extend = "base.toml"
|
||||
|
||||
[lint]
|
||||
extend-select = ["COM812"]
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
"#,
|
||||
@@ -247,11 +275,46 @@ if condition:
|
||||
print('Should change quotes')
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
tab-size = 2
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({filters => vec![
|
||||
(&*regex::escape(ruff_toml.to_str().unwrap()), "[RUFF-TOML-PATH]"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
if True:
|
||||
pass
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
if True:
|
||||
pass
|
||||
|
||||
----- stderr -----
|
||||
warning: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update your configuration to use `indent-width = <value>` instead.
|
||||
"###);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Since 0.1.0 the legacy format option is no longer supported
|
||||
#[test]
|
||||
fn legacy_format_option() -> Result<()> {
|
||||
@@ -291,9 +354,216 @@ format = "json"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
indent-width = 2
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212"]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 3
|
||||
lines-between-types = 2
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = true
|
||||
indent-style = "tab"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_options_stdin() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
indent-width = 2
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212"]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 3
|
||||
lines-between-types = 2
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = true
|
||||
indent-style = "tab"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_linter_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212", "COM812", "ISC001"]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = false
|
||||
quote-style = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_rules_default_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
||||
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff() {
|
||||
let args = ["format", "--isolated", "--diff"];
|
||||
let args = ["format", "--no-cache", "--isolated", "--diff"];
|
||||
let fixtures = Path::new("resources").join("test").join("fixtures");
|
||||
let paths = [
|
||||
fixtures.join("unformatted.py"),
|
||||
@@ -310,8 +580,8 @@ fn test_diff() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
--- resources/test/fixtures/unformatted.ipynb
|
||||
+++ resources/test/fixtures/unformatted.ipynb
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 1
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 1
|
||||
@@ -1,3 +1,4 @@
|
||||
import numpy
|
||||
-maths = (numpy.arange(100)**2).sum()
|
||||
@@ -319,6 +589,30 @@ fn test_diff() {
|
||||
+
|
||||
+maths = (numpy.arange(100) ** 2).sum()
|
||||
+stats = numpy.asarray([1, 2, 3, 4]).median()
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 3
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 3
|
||||
@@ -1,4 +1,6 @@
|
||||
# A cell with IPython escape command
|
||||
def some_function(foo, bar):
|
||||
pass
|
||||
+
|
||||
+
|
||||
%matplotlib inline
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 4
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 4
|
||||
@@ -1,5 +1,10 @@
|
||||
foo = %pwd
|
||||
-def some_function(foo,bar,):
|
||||
+
|
||||
+
|
||||
+def some_function(
|
||||
+ foo,
|
||||
+ bar,
|
||||
+):
|
||||
# Another cell with IPython escape command
|
||||
foo = %pwd
|
||||
print(foo)
|
||||
|
||||
--- resources/test/fixtures/unformatted.py
|
||||
+++ resources/test/fixtures/unformatted.py
|
||||
@@ -1,3 +1,3 @@
|
||||
@@ -327,8 +621,8 @@ fn test_diff() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
2 files would be reformatted, 1 file left unchanged
|
||||
"###);
|
||||
});
|
||||
@@ -336,7 +630,7 @@ fn test_diff() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_no_change() {
|
||||
let args = ["format", "--isolated", "--diff"];
|
||||
let args = ["format", "--no-cache", "--isolated", "--diff"];
|
||||
let fixtures = Path::new("resources").join("test").join("fixtures");
|
||||
let paths = [fixtures.join("unformatted.py")];
|
||||
insta::with_settings!({filters => vec![
|
||||
@@ -357,8 +651,8 @@ fn test_diff_no_change() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
1 file would be reformatted
|
||||
"###
|
||||
);
|
||||
@@ -391,8 +685,8 @@ fn test_diff_stdin_unformatted() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -409,6 +703,5 @@ fn test_diff_stdin_formatted() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -485,7 +485,6 @@ fn stdin_format_jupyter() {
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -635,8 +634,9 @@ fn nursery_group_selector_preview_enabled() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
-:1:2: E225 [*] Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `NURSERY` selector has been deprecated.
|
||||
@@ -655,8 +655,9 @@ fn preview_enabled_prefix() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
-:1:2: E225 [*] Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
@@ -675,8 +676,9 @@ fn preview_enabled_all() {
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:1: D100 Missing docstring in public module
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
-:1:2: E225 [*] Missing whitespace around operator
|
||||
Found 4 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
||||
@@ -695,8 +697,9 @@ fn preview_enabled_direct() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
-:1:2: E225 [*] Missing whitespace around operator
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
@@ -1305,7 +1308,7 @@ extend-unsafe-fixes = ["UP034"]
|
||||
.args([
|
||||
"--output-format",
|
||||
"text",
|
||||
"--no-cache",
|
||||
"--no-cache",
|
||||
"--select",
|
||||
"F601,UP034",
|
||||
])
|
||||
@@ -1344,7 +1347,7 @@ extend-safe-fixes = ["F601"]
|
||||
.args([
|
||||
"--output-format",
|
||||
"text",
|
||||
"--no-cache",
|
||||
"--no-cache",
|
||||
"--select",
|
||||
"F601,UP034",
|
||||
])
|
||||
|
||||
@@ -270,3 +270,41 @@ if __name__ == "__main__":
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_too_long_width_override() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
line-length = 80
|
||||
select = ["E501"]
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 100
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
# longer than 80, but less than 100
|
||||
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜"
|
||||
# longer than 100
|
||||
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜"
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:5:91: E501 Line too long (109 > 100)
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
|
||||
use ruff_python_formatter::{
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions,
|
||||
};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||
|
||||
@@ -871,9 +871,7 @@ struct BlackOptions {
|
||||
line_length: NonZeroU16,
|
||||
#[serde(alias = "skip-magic-trailing-comma")]
|
||||
skip_magic_trailing_comma: bool,
|
||||
#[allow(unused)]
|
||||
#[serde(alias = "force-exclude")]
|
||||
force_exclude: Option<String>,
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
impl Default for BlackOptions {
|
||||
@@ -881,7 +879,7 @@ impl Default for BlackOptions {
|
||||
Self {
|
||||
line_length: NonZeroU16::new(88).unwrap(),
|
||||
skip_magic_trailing_comma: false,
|
||||
force_exclude: None,
|
||||
preview: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,6 +927,11 @@ impl BlackOptions {
|
||||
} else {
|
||||
MagicTrailingComma::Respect
|
||||
})
|
||||
.with_preview(if self.preview {
|
||||
PreviewMode::Enabled
|
||||
} else {
|
||||
PreviewMode::Disabled
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ use crate::ROOT_DIR;
|
||||
const COMMAND_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated command help. -->\n";
|
||||
const COMMAND_HELP_END_PRAGMA: &str = "<!-- End auto-generated command help. -->";
|
||||
|
||||
const SUBCOMMAND_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated subcommand help. -->\n";
|
||||
const SUBCOMMAND_HELP_END_PRAGMA: &str = "<!-- End auto-generated subcommand help. -->";
|
||||
const CHECK_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated check help. -->\n";
|
||||
const CHECK_HELP_END_PRAGMA: &str = "<!-- End auto-generated check help. -->";
|
||||
|
||||
const FORMAT_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated format help. -->\n";
|
||||
const FORMAT_HELP_END_PRAGMA: &str = "<!-- End auto-generated format help. -->";
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
@@ -56,11 +59,15 @@ pub(super) fn main(args: &Args) -> Result<()> {
|
||||
let command_help = trim_lines(&help_text());
|
||||
|
||||
// Generate `ruff help check`.
|
||||
let subcommand_help = trim_lines(&check_help_text());
|
||||
let check_help = trim_lines(&subcommand_help_text("check")?);
|
||||
|
||||
// Generate `ruff help format`.
|
||||
let format_help = trim_lines(&subcommand_help_text("format")?);
|
||||
|
||||
if args.mode.is_dry_run() {
|
||||
print!("{command_help}");
|
||||
print!("{subcommand_help}");
|
||||
print!("{check_help}");
|
||||
print!("{format_help}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -77,9 +84,15 @@ pub(super) fn main(args: &Args) -> Result<()> {
|
||||
)?;
|
||||
let new = replace_docs_section(
|
||||
&new,
|
||||
&format!("```text\n{subcommand_help}\n```\n\n"),
|
||||
SUBCOMMAND_HELP_BEGIN_PRAGMA,
|
||||
SUBCOMMAND_HELP_END_PRAGMA,
|
||||
&format!("```text\n{check_help}\n```\n\n"),
|
||||
CHECK_HELP_BEGIN_PRAGMA,
|
||||
CHECK_HELP_END_PRAGMA,
|
||||
)?;
|
||||
let new = replace_docs_section(
|
||||
&new,
|
||||
&format!("```text\n{format_help}\n```\n\n"),
|
||||
FORMAT_HELP_BEGIN_PRAGMA,
|
||||
FORMAT_HELP_END_PRAGMA,
|
||||
)?;
|
||||
|
||||
match args.mode {
|
||||
@@ -104,18 +117,19 @@ fn help_text() -> String {
|
||||
args::Args::command().render_help().to_string()
|
||||
}
|
||||
|
||||
/// Returns the output of `ruff help check`.
|
||||
fn check_help_text() -> String {
|
||||
/// Returns the output of a given subcommand (e.g., `ruff help check`).
|
||||
fn subcommand_help_text(subcommand: &str) -> Result<String> {
|
||||
let mut cmd = args::Args::command();
|
||||
|
||||
// The build call is necessary for the help output to contain `Usage: ruff
|
||||
// check` instead of `Usage: check` see https://github.com/clap-rs/clap/issues/4685
|
||||
cmd.build();
|
||||
|
||||
cmd.find_subcommand_mut("check")
|
||||
.expect("`check` subcommand not found")
|
||||
Ok(cmd
|
||||
.find_subcommand_mut(subcommand)
|
||||
.with_context(|| format!("Unable to find subcommand `{subcommand}`"))?
|
||||
.render_help()
|
||||
.to_string()
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -22,14 +22,16 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
for rule in rules {
|
||||
let fix_token = match rule.fixable() {
|
||||
FixAvailability::Always | FixAvailability::Sometimes => {
|
||||
format!("<span style='opacity: 1'>{FIX_SYMBOL}</span>")
|
||||
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::None => {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
|
||||
};
|
||||
let preview_token = if rule.is_preview() || rule.is_nursery() {
|
||||
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
|
||||
@@ -2448,7 +2448,7 @@ where
|
||||
|
||||
/// Adds a new entry to the join output.
|
||||
pub fn entry(&mut self, entry: &dyn Format<Context>) -> &mut Self {
|
||||
self.result = self.result.and_then(|_| {
|
||||
self.result = self.result.and_then(|()| {
|
||||
if let Some(with) = &self.with {
|
||||
if self.has_elements {
|
||||
with.fmt(self.fmt)?;
|
||||
@@ -2519,7 +2519,7 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> {
|
||||
separator: &dyn Format<Context>,
|
||||
entry: &dyn Format<Context>,
|
||||
) -> &mut Self {
|
||||
self.result = self.result.and_then(|_| {
|
||||
self.result = self.result.and_then(|()| {
|
||||
if self.empty {
|
||||
self.empty = false;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -79,3 +79,6 @@ from ZeroDivisionError
|
||||
raise IndexError() from ZeroDivisionError
|
||||
|
||||
raise IndexError();
|
||||
|
||||
# RSE102
|
||||
raise Foo()
|
||||
|
||||
@@ -126,3 +126,12 @@ def f():
|
||||
x = yield 3
|
||||
else:
|
||||
x = yield 5
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# OK
|
||||
if TYPE_CHECKING:
|
||||
x = 3
|
||||
else:
|
||||
x = 5
|
||||
|
||||
@@ -38,3 +38,7 @@ env = os.environ['FOO']
|
||||
|
||||
if env := os.environ['FOO']:
|
||||
pass
|
||||
|
||||
os.environ['https_proxy']
|
||||
os.environ.get['http_proxy']
|
||||
os.getenv('no_proxy')
|
||||
|
||||
@@ -114,3 +114,16 @@ elif key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
|
||||
###
|
||||
# Positive cases (preview)
|
||||
###
|
||||
|
||||
# SIM401
|
||||
var = a_dict[key] if key in a_dict else "default3"
|
||||
|
||||
# SIM401
|
||||
var = "default-1" if key not in a_dict else a_dict[key]
|
||||
|
||||
# OK (default contains effect)
|
||||
var = a_dict[key] if key in a_dict else val1 + val2
|
||||
|
||||
@@ -4,18 +4,18 @@ if type(res) == type(42):
|
||||
#: E721
|
||||
if type(res) != type(""):
|
||||
pass
|
||||
#: E721
|
||||
#: Okay
|
||||
import types
|
||||
|
||||
if res == types.IntType:
|
||||
pass
|
||||
#: E721
|
||||
#: Okay
|
||||
import types
|
||||
|
||||
if type(res) is not types.ListType:
|
||||
pass
|
||||
#: E721
|
||||
assert type(res) == type(False)
|
||||
assert type(res) == type(False) or type(res) == type(None)
|
||||
#: E721
|
||||
assert type(res) == type([])
|
||||
#: E721
|
||||
@@ -25,21 +25,18 @@ assert type(res) == type((0,))
|
||||
#: E721
|
||||
assert type(res) == type((0))
|
||||
#: E721
|
||||
assert type(res) != type((1,))
|
||||
#: E721
|
||||
assert type(res) is type((1,))
|
||||
#: E721
|
||||
assert type(res) is not type((1,))
|
||||
assert type(res) != type((1, ))
|
||||
#: Okay
|
||||
assert type(res) is type((1, ))
|
||||
#: Okay
|
||||
assert type(res) is not type((1, ))
|
||||
#: E211 E721
|
||||
assert type(res) == type(
|
||||
[
|
||||
2,
|
||||
]
|
||||
)
|
||||
assert type(res) == type ([2, ])
|
||||
#: E201 E201 E202 E721
|
||||
assert type(res) == type(())
|
||||
assert type(res) == type( ( ) )
|
||||
#: E201 E202 E721
|
||||
assert type(res) == type((0,))
|
||||
assert type(res) == type( (0, ) )
|
||||
#:
|
||||
|
||||
#: Okay
|
||||
import types
|
||||
@@ -50,17 +47,47 @@ if isinstance(res, str):
|
||||
pass
|
||||
if isinstance(res, types.MethodType):
|
||||
pass
|
||||
if type(a) != type(b) or type(a) == type(ccc):
|
||||
#: Okay
|
||||
def func_histype(a, b, c):
|
||||
pass
|
||||
|
||||
assert type(res) == type(None)
|
||||
|
||||
types = StrEnum
|
||||
if x == types.X:
|
||||
#: E722
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
#: E722
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
#: E722 E203 E271
|
||||
try:
|
||||
pass
|
||||
except :
|
||||
pass
|
||||
#: Okay
|
||||
fake_code = """"
|
||||
try:
|
||||
do_something()
|
||||
except:
|
||||
pass
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
#: Okay
|
||||
from . import custom_types as types
|
||||
|
||||
#: E721
|
||||
assert type(res) is int
|
||||
red = types.ColorTypeRED
|
||||
red is types.ColorType.RED
|
||||
#: Okay
|
||||
from . import compute_type
|
||||
|
||||
if compute_type(foo) == 5:
|
||||
pass
|
||||
|
||||
|
||||
class Foo:
|
||||
|
||||
@@ -49,6 +49,13 @@ class Apples:
|
||||
def __doc__(self):
|
||||
return "Docstring"
|
||||
|
||||
# Added in Python 3.12
|
||||
def __buffer__(self):
|
||||
return memoryview(b'')
|
||||
|
||||
def __release_buffer__(self, buf):
|
||||
pass
|
||||
|
||||
# Allow dunder methods recommended by attrs.
|
||||
def __attrs_post_init__(self):
|
||||
pass
|
||||
|
||||
17
crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_module_import.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_module_import.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
from os.path import join as łos # Error
|
||||
from os.path import join as los # OK
|
||||
|
||||
import os.path.join as łos # Error
|
||||
import os.path.join as los # OK
|
||||
|
||||
import os.path.łos # Error (recommend an ASCII alias)
|
||||
import os.path.los # OK
|
||||
|
||||
from os.path import łos # Error (recommend an ASCII alias)
|
||||
from os.path import los # OK
|
||||
|
||||
from os.path.łos import foo # OK
|
||||
from os.path.los import foo # OK
|
||||
|
||||
from os.path import łos as foo # OK
|
||||
from os.path import los as foo # OK
|
||||
31
crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_name.py
vendored
Normal file
31
crates/ruff_linter/resources/test/fixtures/pylint/non_ascii_name.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
ápple_count: int = 1 # C2401
|
||||
ápple_count += 2 # C2401
|
||||
ápple_count = 3 # C2401
|
||||
|
||||
(ápple_count for ápple_count in y)
|
||||
|
||||
|
||||
def func(ápple_count):
|
||||
global ápple_count
|
||||
nonlocal ápple_count
|
||||
|
||||
|
||||
def ápple_count():
|
||||
pass
|
||||
|
||||
|
||||
match ápple_count:
|
||||
case ápple_count:
|
||||
pass
|
||||
|
||||
ápple_count: int
|
||||
|
||||
try:
|
||||
1/0
|
||||
except ápple_count:
|
||||
pass
|
||||
|
||||
# OK
|
||||
print(ápple_count)
|
||||
ápple_count == 3
|
||||
apple_count = 4
|
||||
59
crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py
vendored
Normal file
59
crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
_ = lambda: print() # [unnecessary-lambda]
|
||||
_ = lambda x, y: min(x, y) # [unnecessary-lambda]
|
||||
|
||||
_ = lambda *args: f(*args) # [unnecessary-lambda]
|
||||
_ = lambda **kwargs: f(**kwargs) # [unnecessary-lambda]
|
||||
_ = lambda *args, **kwargs: f(*args, **kwargs) # [unnecessary-lambda]
|
||||
_ = lambda x, y, z, *args, **kwargs: f(x, y, z, *args, **kwargs) # [unnecessary-lambda]
|
||||
|
||||
_ = lambda x: f(lambda x: x)(x) # [unnecessary-lambda]
|
||||
_ = lambda x, y: f(lambda x, y: x + y)(x, y) # [unnecessary-lambda]
|
||||
|
||||
# default value in lambda parameters
|
||||
_ = lambda x=42: print(x)
|
||||
|
||||
# lambda body is not a call
|
||||
_ = lambda x: x
|
||||
|
||||
# ignore chained calls
|
||||
_ = lambda: chained().call()
|
||||
|
||||
# lambda has kwargs but not the call
|
||||
_ = lambda **kwargs: f()
|
||||
|
||||
# lambda has kwargs and the call has kwargs named differently
|
||||
_ = lambda **kwargs: f(**dict([('forty-two', 42)]))
|
||||
|
||||
# lambda has kwargs and the call has unnamed kwargs
|
||||
_ = lambda **kwargs: f(**{1: 2})
|
||||
|
||||
# lambda has starred parameters but not the call
|
||||
_ = lambda *args: f()
|
||||
|
||||
# lambda has starred parameters and the call has starargs named differently
|
||||
_ = lambda *args: f(*list([3, 4]))
|
||||
# lambda has starred parameters and the call has unnamed starargs
|
||||
_ = lambda *args: f(*[3, 4])
|
||||
|
||||
# lambda has parameters but not the call
|
||||
_ = lambda x: f()
|
||||
_ = lambda *x: f()
|
||||
_ = lambda **x: f()
|
||||
|
||||
# lambda parameters and call args are not the same length
|
||||
_ = lambda x, y: f(x)
|
||||
|
||||
# lambda parameters and call args are not in the same order
|
||||
_ = lambda x, y: f(y, x)
|
||||
|
||||
# lambda parameters and call args are not the same
|
||||
_ = lambda x: f(y)
|
||||
|
||||
# the call uses the lambda parameters
|
||||
_ = lambda x: x(x)
|
||||
_ = lambda x, y: x(x, y)
|
||||
_ = lambda x: z(lambda y: x + y)(x)
|
||||
|
||||
# lambda uses an additional keyword
|
||||
_ = lambda *args: f(*args, y=1)
|
||||
_ = lambda *args: f(*args, y=x)
|
||||
@@ -80,11 +80,14 @@ from typing import cast
|
||||
# OK
|
||||
from a import b
|
||||
|
||||
# Ok: `typing_extensions` contains backported improvements.
|
||||
# OK: `typing_extensions` contains backported improvements.
|
||||
from typing_extensions import SupportsIndex
|
||||
|
||||
# Ok: `typing_extensions` contains backported improvements.
|
||||
# OK: `typing_extensions` contains backported improvements.
|
||||
from typing_extensions import NamedTuple
|
||||
|
||||
# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
# OK: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
# UP035
|
||||
from backports.strenum import StrEnum
|
||||
|
||||
@@ -202,3 +202,16 @@ if sys.version_info > (3,12):
|
||||
|
||||
if sys.version_info >= (3,12):
|
||||
print("py3")
|
||||
|
||||
# Slices on `sys.version_info` should be treated equivalently.
|
||||
if sys.version_info[:2] >= (3,0):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info[:3] >= (3,0):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info[:2] > (3,13):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info[:3] > (3,13):
|
||||
print("py3")
|
||||
|
||||
126
crates/ruff_linter/resources/test/fixtures/refurb/FURB101.py
vendored
Normal file
126
crates/ruff_linter/resources/test/fixtures/refurb/FURB101.py
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
def foo():
|
||||
...
|
||||
|
||||
|
||||
def bar(x):
|
||||
...
|
||||
|
||||
|
||||
# Errors.
|
||||
|
||||
# FURB101
|
||||
with open("file.txt") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", "rb") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", mode="rb") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", encoding="utf8") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", errors="ignore") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", errors="ignore", mode="rb") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", mode="r") as f: # noqa: FURB120
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open(foo(), "rb") as f:
|
||||
# The body of `with` is non-trivial, but the recommendation holds.
|
||||
bar("pre")
|
||||
bar(f.read())
|
||||
bar("post")
|
||||
print("Done")
|
||||
|
||||
# FURB101
|
||||
with open("a.txt") as a, open("b.txt", "rb") as b:
|
||||
x = a.read()
|
||||
y = b.read()
|
||||
|
||||
# FURB101
|
||||
with foo() as a, open("file.txt") as b, foo() as c:
|
||||
# We have other things in here, multiple with items, but
|
||||
# the user reads the whole file and that bit they can replace.
|
||||
bar(a)
|
||||
bar(bar(a + b.read()))
|
||||
bar(c)
|
||||
|
||||
|
||||
# Non-errors.
|
||||
|
||||
f2 = open("file2.txt")
|
||||
with open("file.txt") as f:
|
||||
x = f2.read()
|
||||
|
||||
with open("file.txt") as f:
|
||||
# Path.read_text() does not support size, so ignore this
|
||||
x = f.read(100)
|
||||
|
||||
# mode is dynamic
|
||||
with open("file.txt", foo()) as f:
|
||||
x = f.read()
|
||||
|
||||
# keyword mode is incorrect
|
||||
with open("file.txt", mode="a+") as f:
|
||||
x = f.read()
|
||||
|
||||
# enables line buffering, not supported in read_text()
|
||||
with open("file.txt", buffering=1) as f:
|
||||
x = f.read()
|
||||
|
||||
# force CRLF, not supported in read_text()
|
||||
with open("file.txt", newline="\r\n") as f:
|
||||
x = f.read()
|
||||
|
||||
# dont mistake "newline" for "mode"
|
||||
with open("file.txt", newline="b") as f:
|
||||
x = f.read()
|
||||
|
||||
# I guess we can possibly also report this case, but the question
|
||||
# is why the user would put "r+" here in the first place.
|
||||
with open("file.txt", "r+") as f:
|
||||
x = f.read()
|
||||
|
||||
# Even though we read the whole file, we do other things.
|
||||
with open("file.txt") as f:
|
||||
x = f.read()
|
||||
f.seek(0)
|
||||
x += f.read(100)
|
||||
|
||||
# This shouldn't error, since it could contain unsupported arguments, like `buffering`.
|
||||
with open(*filename) as f:
|
||||
x = f.read()
|
||||
|
||||
# This shouldn't error, since it could contain unsupported arguments, like `buffering`.
|
||||
with open(**kwargs) as f:
|
||||
x = f.read()
|
||||
|
||||
# This shouldn't error, since it could contain unsupported arguments, like `buffering`.
|
||||
with open("file.txt", **kwargs) as f:
|
||||
x = f.read()
|
||||
|
||||
# This shouldn't error, since it could contain unsupported arguments, like `buffering`.
|
||||
with open("file.txt", mode="r", **kwargs) as f:
|
||||
x = f.read()
|
||||
|
||||
# This could error (but doesn't), since it can't contain unsupported arguments, like
|
||||
# `buffering`.
|
||||
with open(*filename, mode="r") as f:
|
||||
x = f.read()
|
||||
|
||||
# This could error (but doesn't), since it can't contain unsupported arguments, like
|
||||
# `buffering`.
|
||||
with open(*filename, file="file.txt", mode="r") as f:
|
||||
x = f.read()
|
||||
@@ -31,3 +31,4 @@ print("", "foo", sep=",")
|
||||
print("foo", "", sep=",")
|
||||
print("foo", "", "bar", "", sep=",")
|
||||
print("", "", **kwargs)
|
||||
print(*args, sep=",")
|
||||
|
||||
@@ -21,6 +21,9 @@ def f() -> None:
|
||||
# Invalid (but external)
|
||||
d = 1 # noqa: F841, V101
|
||||
|
||||
# Invalid (but external)
|
||||
d = 1 # noqa: V500
|
||||
|
||||
# fmt: off
|
||||
# Invalid - no space before #
|
||||
d = 1# noqa: E501
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::InvalidAllFormat,
|
||||
Rule::InvalidAllObject,
|
||||
Rule::NonAsciiName,
|
||||
Rule::UnaliasedCollectionsAbcSetImport,
|
||||
Rule::UnconventionalImportAlias,
|
||||
Rule::UnusedVariable,
|
||||
@@ -49,6 +50,11 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::NonAsciiName) {
|
||||
if let Some(diagnostic) = pylint::rules::non_ascii_name(binding, checker.locator) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::UnconventionalImportAlias) {
|
||||
if let Some(diagnostic) = flake8_import_conventions::rules::unconventional_import_alias(
|
||||
checker,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use ruff_python_ast::Expr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_pie, pylint};
|
||||
|
||||
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
|
||||
while !checker.deferred.lambdas.is_empty() {
|
||||
let lambdas = std::mem::take(&mut checker.deferred.lambdas);
|
||||
for snapshot in lambdas {
|
||||
checker.semantic.restore(snapshot);
|
||||
|
||||
let Some(Expr::Lambda(lambda)) = checker.semantic.current_expression() else {
|
||||
unreachable!("Expected Expr::Lambda");
|
||||
};
|
||||
|
||||
if checker.enabled(Rule::UnnecessaryLambda) {
|
||||
pylint::rules::unnecessary_lambda(checker, lambda);
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedListBuiltin) {
|
||||
flake8_pie::rules::reimplemented_list_builtin(checker, lambda);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1267,23 +1267,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Lambda(
|
||||
lambda @ ast::ExprLambda {
|
||||
parameters: _,
|
||||
body: _,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.enabled(Rule::ReimplementedListBuiltin) {
|
||||
flake8_pie::rules::reimplemented_list_builtin(checker, lambda);
|
||||
}
|
||||
}
|
||||
Expr::IfExp(ast::ExprIfExp {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) => {
|
||||
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
|
||||
flake8_simplify::rules::if_exp_instead_of_dict_get(
|
||||
checker, expr, test, body, orelse,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::IfExprWithTrueFalse) {
|
||||
flake8_simplify::rules::if_expr_with_true_false(checker, expr, test, body, orelse);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub(super) use bindings::bindings;
|
||||
pub(super) use comprehension::comprehension;
|
||||
pub(super) use deferred_for_loops::deferred_for_loops;
|
||||
pub(super) use deferred_lambdas::deferred_lambdas;
|
||||
pub(super) use deferred_scopes::deferred_scopes;
|
||||
pub(super) use definitions::definitions;
|
||||
pub(super) use except_handler::except_handler;
|
||||
@@ -15,6 +16,7 @@ pub(super) use unresolved_references::unresolved_references;
|
||||
mod bindings;
|
||||
mod comprehension;
|
||||
mod deferred_for_loops;
|
||||
mod deferred_lambdas;
|
||||
mod deferred_scopes;
|
||||
mod definitions;
|
||||
mod except_handler;
|
||||
|
||||
@@ -547,6 +547,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if checker.enabled(Rule::NonAsciiImportName) {
|
||||
pylint::rules::non_ascii_module_import(checker, alias);
|
||||
}
|
||||
if let Some(asname) = &alias.asname {
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(
|
||||
@@ -698,8 +701,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
let module = module.as_deref();
|
||||
let level = *level;
|
||||
let module = module.as_deref();
|
||||
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
|
||||
}
|
||||
@@ -712,6 +715,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::NonAsciiImportName) {
|
||||
for alias in names {
|
||||
pylint::rules::non_ascii_module_import(checker, alias);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryFutureImport) {
|
||||
if checker.settings.target_version >= PythonVersion::Py37 {
|
||||
if let Some("__future__") = module {
|
||||
@@ -1044,16 +1052,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_);
|
||||
}
|
||||
if checker.enabled(Rule::NeedlessBool) {
|
||||
flake8_simplify::rules::needless_bool(checker, stmt);
|
||||
flake8_simplify::rules::needless_bool(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::IfElseBlockInsteadOfDictLookup) {
|
||||
flake8_simplify::rules::manual_dict_lookup(checker, if_);
|
||||
flake8_simplify::rules::if_else_block_instead_of_dict_lookup(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::IfElseBlockInsteadOfIfExp) {
|
||||
flake8_simplify::rules::use_ternary_operator(checker, stmt);
|
||||
flake8_simplify::rules::if_else_block_instead_of_if_exp(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
|
||||
flake8_simplify::rules::use_dict_get_with_default(checker, if_);
|
||||
flake8_simplify::rules::if_else_block_instead_of_dict_get(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::TypeCheckWithoutTypeError) {
|
||||
tryceratops::rules::type_check_without_type_error(
|
||||
@@ -1175,6 +1183,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::RedefinedLoopName) {
|
||||
pylint::rules::redefined_loop_name(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::ReadWholeFile) {
|
||||
refurb::rules::read_whole_file(checker, with_stmt);
|
||||
}
|
||||
}
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if checker.enabled(Rule::FunctionUsesLoopVariable) {
|
||||
@@ -1314,6 +1325,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => {
|
||||
checker.enabled(Rule::NonAsciiName);
|
||||
if checker.enabled(Rule::LambdaAssignment) {
|
||||
if let [target] = &targets[..] {
|
||||
pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt);
|
||||
|
||||
@@ -12,6 +12,6 @@ pub(crate) struct Deferred<'a> {
|
||||
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) type_param_definitions: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) functions: Vec<Snapshot>,
|
||||
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) lambdas: Vec<Snapshot>,
|
||||
pub(crate) for_loops: Vec<Snapshot>,
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
|
||||
use ruff_python_semantic::analyze::{typing, visibility};
|
||||
use ruff_python_semantic::{
|
||||
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
|
||||
SubmoduleImport,
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
|
||||
StarImport, SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_source_file::Locator;
|
||||
@@ -902,7 +902,7 @@ where
|
||||
}
|
||||
|
||||
self.semantic.push_scope(ScopeKind::Lambda(lambda));
|
||||
self.deferred.lambdas.push((expr, self.semantic.snapshot()));
|
||||
self.deferred.lambdas.push(self.semantic.snapshot());
|
||||
}
|
||||
Expr::IfExp(ast::ExprIfExp {
|
||||
test,
|
||||
@@ -1834,15 +1834,15 @@ impl<'a> Checker<'a> {
|
||||
for snapshot in deferred_functions {
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if let Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
let Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
body, parameters, ..
|
||||
}) = self.semantic.current_statement()
|
||||
{
|
||||
self.visit_parameters(parameters);
|
||||
self.visit_body(body);
|
||||
} else {
|
||||
else {
|
||||
unreachable!("Expected Stmt::FunctionDef")
|
||||
}
|
||||
};
|
||||
|
||||
self.visit_parameters(parameters);
|
||||
self.visit_body(body);
|
||||
}
|
||||
}
|
||||
self.semantic.restore(snapshot);
|
||||
@@ -1850,26 +1850,31 @@ impl<'a> Checker<'a> {
|
||||
|
||||
fn visit_deferred_lambdas(&mut self) {
|
||||
let snapshot = self.semantic.snapshot();
|
||||
let mut deferred: Vec<Snapshot> = Vec::with_capacity(self.deferred.lambdas.len());
|
||||
while !self.deferred.lambdas.is_empty() {
|
||||
let lambdas = std::mem::take(&mut self.deferred.lambdas);
|
||||
for (expr, snapshot) in lambdas {
|
||||
for snapshot in lambdas {
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if let Expr::Lambda(ast::ExprLambda {
|
||||
let Some(Expr::Lambda(ast::ExprLambda {
|
||||
parameters,
|
||||
body,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
if let Some(parameters) = parameters {
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.visit_expr(body);
|
||||
} else {
|
||||
})) = self.semantic.current_expression()
|
||||
else {
|
||||
unreachable!("Expected Expr::Lambda");
|
||||
};
|
||||
|
||||
if let Some(parameters) = parameters {
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.visit_expr(body);
|
||||
|
||||
deferred.push(snapshot);
|
||||
}
|
||||
}
|
||||
// Reset the deferred lambdas, so we can analyze them later on.
|
||||
self.deferred.lambdas = deferred;
|
||||
self.semantic.restore(snapshot);
|
||||
}
|
||||
|
||||
@@ -1989,6 +1994,7 @@ pub(crate) fn check_ast(
|
||||
checker.visit_exports();
|
||||
|
||||
// Check docstrings, bindings, and unresolved references.
|
||||
analyze::deferred_lambdas(&mut checker);
|
||||
analyze::deferred_for_loops(&mut checker);
|
||||
analyze::definitions(&mut checker);
|
||||
analyze::bindings(&mut checker);
|
||||
|
||||
@@ -128,7 +128,10 @@ pub(crate) fn check_noqa(
|
||||
}
|
||||
|
||||
if line.matches.iter().any(|match_| *match_ == code)
|
||||
|| settings.external.contains(code)
|
||||
|| settings
|
||||
.external
|
||||
.iter()
|
||||
.any(|external| code.starts_with(external))
|
||||
{
|
||||
valid_codes.push(code);
|
||||
} else {
|
||||
|
||||
@@ -95,6 +95,7 @@ mod tests {
|
||||
|
||||
use crate::line_width::LineLength;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pycodestyle;
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
use super::check_physical_lines;
|
||||
@@ -114,7 +115,10 @@ mod tests {
|
||||
&indexer,
|
||||
&[],
|
||||
&LinterSettings {
|
||||
line_length,
|
||||
pycodestyle: pycodestyle::settings::Settings {
|
||||
max_line_length: line_length,
|
||||
..pycodestyle::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::LineTooLong)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -211,6 +211,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots),
|
||||
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
|
||||
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
|
||||
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
|
||||
@@ -258,6 +260,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
||||
(Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda),
|
||||
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable),
|
||||
(Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral),
|
||||
@@ -921,6 +924,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Slots, "002") => (RuleGroup::Stable, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
|
||||
|
||||
// refurb
|
||||
(Refurb, "101") => (RuleGroup::Preview, rules::refurb::rules::ReadWholeFile),
|
||||
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
|
||||
@@ -14,7 +14,7 @@ use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::fix::codemods;
|
||||
use crate::line_width::{LineLength, LineWidthBuilder, TabSize};
|
||||
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
|
||||
|
||||
/// Return the `Fix` to use when deleting a `Stmt`.
|
||||
///
|
||||
@@ -293,7 +293,7 @@ pub(crate) fn fits(
|
||||
node: AnyNodeRef,
|
||||
locator: &Locator,
|
||||
line_length: LineLength,
|
||||
tab_size: TabSize,
|
||||
tab_size: IndentWidth,
|
||||
) -> bool {
|
||||
all_lines_fit(fix, node, locator, line_length.value() as usize, tab_size)
|
||||
}
|
||||
@@ -305,7 +305,7 @@ pub(crate) fn fits_or_shrinks(
|
||||
node: AnyNodeRef,
|
||||
locator: &Locator,
|
||||
line_length: LineLength,
|
||||
tab_size: TabSize,
|
||||
tab_size: IndentWidth,
|
||||
) -> bool {
|
||||
// Use the larger of the line length limit, or the longest line in the existing AST node.
|
||||
let line_length = std::iter::once(line_length.value() as usize)
|
||||
@@ -327,7 +327,7 @@ fn all_lines_fit(
|
||||
node: AnyNodeRef,
|
||||
locator: &Locator,
|
||||
line_length: usize,
|
||||
tab_size: TabSize,
|
||||
tab_size: IndentWidth,
|
||||
) -> bool {
|
||||
let prefix = locator.slice(TextRange::new(
|
||||
locator.line_start(node.start()),
|
||||
|
||||
@@ -129,12 +129,12 @@ pub struct LineWidthBuilder {
|
||||
/// This is used to calculate the width of tabs.
|
||||
column: usize,
|
||||
/// The tab size to use when calculating the width of tabs.
|
||||
tab_size: TabSize,
|
||||
tab_size: IndentWidth,
|
||||
}
|
||||
|
||||
impl Default for LineWidthBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new(TabSize::default())
|
||||
Self::new(IndentWidth::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ impl LineWidthBuilder {
|
||||
}
|
||||
|
||||
/// Creates a new `LineWidth` with the given tab size.
|
||||
pub fn new(tab_size: TabSize) -> Self {
|
||||
pub fn new(tab_size: IndentWidth) -> Self {
|
||||
LineWidthBuilder {
|
||||
width: 0,
|
||||
column: 0,
|
||||
@@ -234,28 +234,28 @@ impl PartialOrd<LineLength> for LineWidthBuilder {
|
||||
/// The size of a tab.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TabSize(NonZeroU8);
|
||||
pub struct IndentWidth(NonZeroU8);
|
||||
|
||||
impl TabSize {
|
||||
impl IndentWidth {
|
||||
pub(crate) fn as_usize(self) -> usize {
|
||||
self.0.get() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TabSize {
|
||||
impl Default for IndentWidth {
|
||||
fn default() -> Self {
|
||||
Self(NonZeroU8::new(4).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NonZeroU8> for TabSize {
|
||||
impl From<NonZeroU8> for IndentWidth {
|
||||
fn from(tab_size: NonZeroU8) -> Self {
|
||||
Self(tab_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TabSize> for NonZeroU8 {
|
||||
fn from(value: TabSize) -> Self {
|
||||
impl From<IndentWidth> for NonZeroU8 {
|
||||
fn from(value: IndentWidth) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use ruff_source_file::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::line_width::{LineWidthBuilder, TabSize};
|
||||
use crate::line_width::{IndentWidth, LineWidthBuilder};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
@@ -300,7 +300,7 @@ fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
let mut range = annotation_range;
|
||||
let mut line_width = LineWidthBuilder::new(TabSize::default());
|
||||
let mut line_width = LineWidthBuilder::new(IndentWidth::default());
|
||||
|
||||
for (index, c) in source.char_indices() {
|
||||
let old_width = line_width.get();
|
||||
|
||||
@@ -30,6 +30,12 @@ use crate::checkers::ast::Checker;
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Mutable argument defaults can be used intentionally to cache computation
|
||||
/// results. Replacing the default with `None` or an immutable data structure
|
||||
/// does not work for such usages. Instead, prefer the `@functools.lru_cache`
|
||||
/// decorator from the standard library.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def add_to_list(item, some_list=[]):
|
||||
@@ -197,5 +203,5 @@ fn move_initialization(
|
||||
}
|
||||
|
||||
let initialization_edit = Edit::insertion(content, pos);
|
||||
Some(Fix::display_edits(default_edit, [initialization_edit]))
|
||||
Some(Fix::unsafe_edits(default_edit, [initialization_edit]))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_1.py:3:22: B006 Do not use mutable data structures for argument defaults
|
||||
B006_1.py:3:22: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
1 | # Docstring followed by a newline
|
||||
2 |
|
||||
@@ -12,7 +12,7 @@ B006_1.py:3:22: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
1 1 | # Docstring followed by a newline
|
||||
2 2 |
|
||||
3 |-def foobar(foor, bar={}):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_2.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
B006_2.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
|
||||
3 |
|
||||
@@ -12,7 +12,7 @@ B006_2.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
1 1 | # Docstring followed by whitespace with no newline
|
||||
2 2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
|
||||
3 3 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_3.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
B006_3.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
4 | def foobar(foor, bar={}):
|
||||
| ^^ B006
|
||||
@@ -10,7 +10,7 @@ B006_3.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
1 1 | # Docstring with no newline
|
||||
2 2 |
|
||||
3 3 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_4.py:7:26: B006 Do not use mutable data structures for argument defaults
|
||||
B006_4.py:7:26: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
6 | class FormFeedIndent:
|
||||
7 | def __init__(self, a=[]):
|
||||
@@ -10,7 +10,7 @@ B006_4.py:7:26: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
4 4 |
|
||||
5 5 |
|
||||
6 6 | class FormFeedIndent:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_5.py:5:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:5:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
5 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -9,7 +9,7 @@ B006_5.py:5:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
2 2 | # https://github.com/astral-sh/ruff/issues/7616
|
||||
3 3 |
|
||||
4 4 |
|
||||
@@ -22,7 +22,7 @@ B006_5.py:5:49: B006 Do not use mutable data structures for argument defaults
|
||||
8 10 |
|
||||
9 11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:9:61: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:9:61: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
9 | def import_module_with_values_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -30,7 +30,7 @@ B006_5.py:9:61: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
6 6 | import os
|
||||
7 7 |
|
||||
8 8 |
|
||||
@@ -44,7 +44,7 @@ B006_5.py:9:61: B006 Do not use mutable data structures for argument defaults
|
||||
13 15 |
|
||||
14 16 |
|
||||
|
||||
B006_5.py:15:50: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:15:50: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
15 | def import_modules_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -53,7 +53,7 @@ B006_5.py:15:50: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
12 12 | return 2
|
||||
13 13 |
|
||||
14 14 |
|
||||
@@ -68,7 +68,7 @@ B006_5.py:15:50: B006 Do not use mutable data structures for argument defaults
|
||||
20 22 |
|
||||
21 23 | def from_import_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:21:54: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:21:54: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
21 | def from_import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -76,7 +76,7 @@ B006_5.py:21:54: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
18 18 | import itertools
|
||||
19 19 |
|
||||
20 20 |
|
||||
@@ -89,7 +89,7 @@ B006_5.py:21:54: B006 Do not use mutable data structures for argument defaults
|
||||
24 26 |
|
||||
25 27 | def from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:25:55: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:25:55: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
25 | def from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -98,7 +98,7 @@ B006_5.py:25:55: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
22 22 | from os import path
|
||||
23 23 |
|
||||
24 24 |
|
||||
@@ -112,7 +112,7 @@ B006_5.py:25:55: B006 Do not use mutable data structures for argument defaults
|
||||
29 31 |
|
||||
30 32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:30:66: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:30:66: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
30 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -121,7 +121,7 @@ B006_5.py:30:66: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
27 27 | from sys import version_info
|
||||
28 28 |
|
||||
29 29 |
|
||||
@@ -135,7 +135,7 @@ B006_5.py:30:66: B006 Do not use mutable data structures for argument defaults
|
||||
34 36 |
|
||||
35 37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:35:59: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:35:59: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
35 | def import_docstring_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -144,7 +144,7 @@ B006_5.py:35:59: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
32 32 | from sys import version_info
|
||||
33 33 |
|
||||
34 34 |
|
||||
@@ -158,7 +158,7 @@ B006_5.py:35:59: B006 Do not use mutable data structures for argument defaults
|
||||
39 41 |
|
||||
40 42 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:40:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:40:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
40 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -167,7 +167,7 @@ B006_5.py:40:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
37 37 | import os
|
||||
38 38 |
|
||||
39 39 |
|
||||
@@ -181,7 +181,7 @@ B006_5.py:40:49: B006 Do not use mutable data structures for argument defaults
|
||||
44 46 |
|
||||
45 47 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:45:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:45:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
45 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -190,7 +190,7 @@ B006_5.py:45:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
42 42 | import os; import sys
|
||||
43 43 |
|
||||
44 44 |
|
||||
@@ -203,7 +203,7 @@ B006_5.py:45:49: B006 Do not use mutable data structures for argument defaults
|
||||
48 50 |
|
||||
49 51 |
|
||||
|
||||
B006_5.py:50:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:50:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
50 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -212,7 +212,7 @@ B006_5.py:50:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
47 47 | import os; import sys; x = 1
|
||||
48 48 |
|
||||
49 49 |
|
||||
@@ -226,7 +226,7 @@ B006_5.py:50:49: B006 Do not use mutable data structures for argument defaults
|
||||
54 56 |
|
||||
55 57 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:55:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:55:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
55 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -234,7 +234,7 @@ B006_5.py:55:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
52 52 | import os; import sys
|
||||
53 53 |
|
||||
54 54 |
|
||||
@@ -247,7 +247,7 @@ B006_5.py:55:49: B006 Do not use mutable data structures for argument defaults
|
||||
58 60 |
|
||||
59 61 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_5.py:59:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:59:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
59 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -255,7 +255,7 @@ B006_5.py:59:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
56 56 | import os; import sys
|
||||
57 57 |
|
||||
58 58 |
|
||||
@@ -267,7 +267,7 @@ B006_5.py:59:49: B006 Do not use mutable data structures for argument defaults
|
||||
61 63 |
|
||||
62 64 |
|
||||
|
||||
B006_5.py:63:49: B006 Do not use mutable data structures for argument defaults
|
||||
B006_5.py:63:49: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
63 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -275,7 +275,7 @@ B006_5.py:63:49: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
60 60 | import os; import sys; x = 1
|
||||
61 61 |
|
||||
62 62 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_6.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
B006_6.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
2 | # Same as B006_2.py, but import instead of docstring
|
||||
3 |
|
||||
@@ -11,7 +11,7 @@ B006_6.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
1 1 | # Import followed by whitespace with no newline
|
||||
2 2 | # Same as B006_2.py, but import instead of docstring
|
||||
3 3 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_7.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
B006_7.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
2 | # Same as B006_3.py, but import instead of docstring
|
||||
3 |
|
||||
@@ -11,7 +11,7 @@ B006_7.py:4:22: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
1 1 | # Import with no newline
|
||||
2 2 | # Same as B006_3.py, but import instead of docstring
|
||||
3 3 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
63 | def this_is_wrong(value=[1, 2, 3]):
|
||||
| ^^^^^^^^^ B006
|
||||
@@ -9,7 +9,7 @@ B006_B008.py:63:25: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
60 60 | # Flag mutable literals/comprehensions
|
||||
61 61 |
|
||||
62 62 |
|
||||
@@ -21,7 +21,7 @@ B006_B008.py:63:25: B006 Do not use mutable data structures for argument default
|
||||
65 67 |
|
||||
66 68 |
|
||||
|
||||
B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
67 | def this_is_also_wrong(value={}):
|
||||
| ^^ B006
|
||||
@@ -29,7 +29,7 @@ B006_B008.py:67:30: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
64 64 | ...
|
||||
65 65 |
|
||||
66 66 |
|
||||
@@ -41,7 +41,7 @@ B006_B008.py:67:30: B006 Do not use mutable data structures for argument default
|
||||
69 71 |
|
||||
70 72 |
|
||||
|
||||
B006_B008.py:73:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
71 | class Foo:
|
||||
72 | @staticmethod
|
||||
@@ -51,7 +51,7 @@ B006_B008.py:73:52: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
70 70 |
|
||||
71 71 | class Foo:
|
||||
72 72 | @staticmethod
|
||||
@@ -63,7 +63,7 @@ B006_B008.py:73:52: B006 Do not use mutable data structures for argument default
|
||||
75 77 |
|
||||
76 78 |
|
||||
|
||||
B006_B008.py:77:31: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
77 | def multiline_arg_wrong(value={
|
||||
| _______________________________^
|
||||
@@ -74,7 +74,7 @@ B006_B008.py:77:31: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
74 74 | pass
|
||||
75 75 |
|
||||
76 76 |
|
||||
@@ -97,7 +97,7 @@ B006_B008.py:82:36: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:85:20: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
85 | def and_this(value=set()):
|
||||
| ^^^^^ B006
|
||||
@@ -105,7 +105,7 @@ B006_B008.py:85:20: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
82 82 | def single_line_func_wrong(value = {}): ...
|
||||
83 83 |
|
||||
84 84 |
|
||||
@@ -117,7 +117,7 @@ B006_B008.py:85:20: B006 Do not use mutable data structures for argument default
|
||||
87 89 |
|
||||
88 90 |
|
||||
|
||||
B006_B008.py:89:20: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
89 | def this_too(value=collections.OrderedDict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
@@ -125,7 +125,7 @@ B006_B008.py:89:20: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
86 86 | ...
|
||||
87 87 |
|
||||
88 88 |
|
||||
@@ -137,7 +137,7 @@ B006_B008.py:89:20: B006 Do not use mutable data structures for argument default
|
||||
91 93 |
|
||||
92 94 |
|
||||
|
||||
B006_B008.py:93:32: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
93 | async def async_this_too(value=collections.defaultdict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
@@ -145,7 +145,7 @@ B006_B008.py:93:32: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
90 90 | ...
|
||||
91 91 |
|
||||
92 92 |
|
||||
@@ -157,7 +157,7 @@ B006_B008.py:93:32: B006 Do not use mutable data structures for argument default
|
||||
95 97 |
|
||||
96 98 |
|
||||
|
||||
B006_B008.py:97:26: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
97 | def dont_forget_me(value=collections.deque()):
|
||||
| ^^^^^^^^^^^^^^^^^^^ B006
|
||||
@@ -165,7 +165,7 @@ B006_B008.py:97:26: B006 Do not use mutable data structures for argument default
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
94 94 | ...
|
||||
95 95 |
|
||||
96 96 |
|
||||
@@ -177,7 +177,7 @@ B006_B008.py:97:26: B006 Do not use mutable data structures for argument default
|
||||
99 101 |
|
||||
100 102 |
|
||||
|
||||
B006_B008.py:102:46: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
101 | # N.B. we're also flagging the function call in the comprehension
|
||||
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
@@ -186,7 +186,7 @@ B006_B008.py:102:46: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
99 99 |
|
||||
100 100 |
|
||||
101 101 | # N.B. we're also flagging the function call in the comprehension
|
||||
@@ -198,7 +198,7 @@ B006_B008.py:102:46: B006 Do not use mutable data structures for argument defaul
|
||||
104 106 |
|
||||
105 107 |
|
||||
|
||||
B006_B008.py:106:46: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
@@ -206,7 +206,7 @@ B006_B008.py:106:46: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
103 103 | pass
|
||||
104 104 |
|
||||
105 105 |
|
||||
@@ -218,7 +218,7 @@ B006_B008.py:106:46: B006 Do not use mutable data structures for argument defaul
|
||||
108 110 |
|
||||
109 111 |
|
||||
|
||||
B006_B008.py:110:45: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
@@ -226,7 +226,7 @@ B006_B008.py:110:45: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
107 107 | pass
|
||||
108 108 |
|
||||
109 109 |
|
||||
@@ -238,7 +238,7 @@ B006_B008.py:110:45: B006 Do not use mutable data structures for argument defaul
|
||||
112 114 |
|
||||
113 115 |
|
||||
|
||||
B006_B008.py:114:33: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
114 | def kwonlyargs_mutable(*, value=[]):
|
||||
| ^^ B006
|
||||
@@ -246,7 +246,7 @@ B006_B008.py:114:33: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
111 111 | pass
|
||||
112 112 |
|
||||
113 113 |
|
||||
@@ -258,7 +258,7 @@ B006_B008.py:114:33: B006 Do not use mutable data structures for argument defaul
|
||||
116 118 |
|
||||
117 119 |
|
||||
|
||||
B006_B008.py:239:20: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
237 | # B006 and B008
|
||||
238 | # We should handle arbitrary nesting of these B008.
|
||||
@@ -268,7 +268,7 @@ B006_B008.py:239:20: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
236 236 |
|
||||
237 237 | # B006 and B008
|
||||
238 238 | # We should handle arbitrary nesting of these B008.
|
||||
@@ -280,7 +280,7 @@ B006_B008.py:239:20: B006 Do not use mutable data structures for argument defaul
|
||||
241 243 |
|
||||
242 244 |
|
||||
|
||||
B006_B008.py:276:27: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
@@ -290,7 +290,7 @@ B006_B008.py:276:27: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
273 273 |
|
||||
274 274 |
|
||||
275 275 | def mutable_annotations(
|
||||
@@ -306,7 +306,7 @@ B006_B008.py:276:27: B006 Do not use mutable data structures for argument defaul
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:277:35: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
@@ -317,7 +317,7 @@ B006_B008.py:277:35: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
274 274 |
|
||||
275 275 | def mutable_annotations(
|
||||
276 276 | a: list[int] | None = [],
|
||||
@@ -332,7 +332,7 @@ B006_B008.py:277:35: B006 Do not use mutable data structures for argument defaul
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:278:62: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
@@ -343,7 +343,7 @@ B006_B008.py:278:62: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
275 275 | def mutable_annotations(
|
||||
276 276 | a: list[int] | None = [],
|
||||
277 277 | b: Optional[Dict[int, int]] = {},
|
||||
@@ -357,7 +357,7 @@ B006_B008.py:278:62: B006 Do not use mutable data structures for argument defaul
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:279:80: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
@@ -368,7 +368,7 @@ B006_B008.py:279:80: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
276 276 | a: list[int] | None = [],
|
||||
277 277 | b: Optional[Dict[int, int]] = {},
|
||||
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
@@ -381,7 +381,7 @@ B006_B008.py:279:80: B006 Do not use mutable data structures for argument defaul
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:284:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
284 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -389,7 +389,7 @@ B006_B008.py:284:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
281 281 | pass
|
||||
282 282 |
|
||||
283 283 |
|
||||
@@ -402,7 +402,7 @@ B006_B008.py:284:52: B006 Do not use mutable data structures for argument defaul
|
||||
287 289 |
|
||||
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_B008.py:288:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
288 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -411,7 +411,7 @@ B006_B008.py:288:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
285 285 | """Docstring"""
|
||||
286 286 |
|
||||
287 287 |
|
||||
@@ -424,7 +424,7 @@ B006_B008.py:288:52: B006 Do not use mutable data structures for argument defaul
|
||||
291 293 |
|
||||
292 294 |
|
||||
|
||||
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:293:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
293 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -432,7 +432,7 @@ B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
290 290 | ...
|
||||
291 291 |
|
||||
292 292 |
|
||||
@@ -444,7 +444,7 @@ B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaul
|
||||
295 297 |
|
||||
296 298 |
|
||||
|
||||
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:297:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
297 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -453,7 +453,7 @@ B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
294 294 | """Docstring"""; ...
|
||||
295 295 |
|
||||
296 296 |
|
||||
@@ -465,7 +465,7 @@ B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaul
|
||||
299 301 | ...
|
||||
300 302 |
|
||||
|
||||
B006_B008.py:302:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
302 | def single_line_func_wrong(value: dict[str, str] = {
|
||||
| ____________________________________________________^
|
||||
@@ -476,7 +476,7 @@ B006_B008.py:302:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
299 299 | ...
|
||||
300 300 |
|
||||
301 301 |
|
||||
@@ -500,7 +500,7 @@ B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:313:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:313:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
313 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
@@ -508,7 +508,7 @@ B006_B008.py:313:52: B006 Do not use mutable data structures for argument defaul
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
310 310 | """Docstring"""
|
||||
311 311 |
|
||||
312 312 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_extended.py:17:55: B006 Do not use mutable data structures for argument defaults
|
||||
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
|
||||
| ^^ B006
|
||||
@@ -9,7 +9,7 @@ B006_extended.py:17:55: B006 Do not use mutable data structures for argument def
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
ℹ Suggested fix
|
||||
14 14 | ...
|
||||
15 15 |
|
||||
16 16 |
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::settings::LinterSettings;
|
||||
/// negatively affects code readability.
|
||||
///
|
||||
/// In some cases, the implicit concatenation may also be unintentional, as
|
||||
/// autoformatters are capable of introducing single-line implicit
|
||||
/// code formatters are capable of introducing single-line implicit
|
||||
/// concatenations when collapsing long lines.
|
||||
///
|
||||
/// ## Example
|
||||
|
||||
@@ -27,6 +27,9 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// tf.keras.backend
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-import-conventions.banned-aliases`
|
||||
#[violation]
|
||||
pub struct BannedImportAlias {
|
||||
name: String,
|
||||
|
||||
@@ -28,6 +28,9 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// pd.Series
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-import-conventions.banned-from`
|
||||
#[violation]
|
||||
pub struct BannedImportFrom {
|
||||
name: String,
|
||||
|
||||
@@ -30,6 +30,10 @@ use crate::renamer::Renamer;
|
||||
/// ```python
|
||||
/// import pandas as pd
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-import-conventions.aliases`
|
||||
/// - `flake8-import-conventions.extend-aliases`
|
||||
#[violation]
|
||||
pub struct UnconventionalImportAlias {
|
||||
name: String,
|
||||
|
||||
@@ -28,7 +28,7 @@ use super::helpers::{
|
||||
/// setting.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If a `@pytext.fixture()` doesn't take any arguments, the parentheses are
|
||||
/// If a `@pytest.fixture()` doesn't take any arguments, the parentheses are
|
||||
/// optional.
|
||||
///
|
||||
/// Either removing those unnecessary parentheses _or_ requiring them for all
|
||||
@@ -900,10 +900,9 @@ fn check_fixture_addfinalizer(checker: &mut Checker, parameters: &Parameters, bo
|
||||
|
||||
/// PT024, PT025
|
||||
fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) {
|
||||
for (expr, call_path) in get_mark_decorators(decorators) {
|
||||
let name = call_path.last().expect("Expected a mark name");
|
||||
for (expr, marker) in get_mark_decorators(decorators) {
|
||||
if checker.enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) {
|
||||
if *name == "asyncio" {
|
||||
if marker == "asyncio" {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, expr.range());
|
||||
let range = checker.locator().full_lines_range(expr.range());
|
||||
@@ -913,7 +912,7 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) {
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::PytestErroneousUseFixturesOnFixture) {
|
||||
if *name == "usefixtures" {
|
||||
if marker == "usefixtures" {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(PytestErroneousUseFixturesOnFixture, expr.range());
|
||||
let line_range = checker.locator().full_lines_range(expr.range());
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use ruff_python_ast::{self as ast, Constant, Decorator, Expr, Keyword};
|
||||
|
||||
use ruff_python_ast::call_path::{collect_call_path, CallPath};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_trivia::PythonWhitespace;
|
||||
|
||||
pub(super) fn get_mark_decorators(
|
||||
decorators: &[Decorator],
|
||||
) -> impl Iterator<Item = (&Decorator, CallPath)> {
|
||||
) -> impl Iterator<Item = (&Decorator, &str)> {
|
||||
decorators.iter().filter_map(|decorator| {
|
||||
let Some(call_path) = collect_call_path(map_callable(&decorator.expression)) else {
|
||||
return None;
|
||||
};
|
||||
if call_path.len() > 2 && call_path.as_slice()[..2] == ["pytest", "mark"] {
|
||||
Some((decorator, call_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let ["pytest", "mark", marker] = call_path.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
Some((decorator, *marker))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use ruff_python_ast::{self as ast, Arguments, Decorator, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -117,14 +116,14 @@ impl AlwaysFixableViolation for PytestUseFixturesWithoutParameters {
|
||||
fn pytest_mark_parentheses(
|
||||
checker: &mut Checker,
|
||||
decorator: &Decorator,
|
||||
call_path: &CallPath,
|
||||
marker: &str,
|
||||
fix: Fix,
|
||||
preferred: &str,
|
||||
actual: &str,
|
||||
) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestIncorrectMarkParenthesesStyle {
|
||||
mark_name: (*call_path.last().unwrap()).to_string(),
|
||||
mark_name: marker.to_string(),
|
||||
expected_parens: preferred.to_string(),
|
||||
actual_parens: actual.to_string(),
|
||||
},
|
||||
@@ -134,7 +133,7 @@ fn pytest_mark_parentheses(
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
fn check_mark_parentheses(checker: &mut Checker, decorator: &Decorator, call_path: &CallPath) {
|
||||
fn check_mark_parentheses(checker: &mut Checker, decorator: &Decorator, marker: &str) {
|
||||
match &decorator.expression {
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
@@ -151,52 +150,53 @@ fn check_mark_parentheses(checker: &mut Checker, decorator: &Decorator, call_pat
|
||||
&& keywords.is_empty()
|
||||
{
|
||||
let fix = Fix::safe_edit(Edit::deletion(func.end(), decorator.end()));
|
||||
pytest_mark_parentheses(checker, decorator, call_path, fix, "", "()");
|
||||
pytest_mark_parentheses(checker, decorator, marker, fix, "", "()");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if checker.settings.flake8_pytest_style.mark_parentheses {
|
||||
let fix = Fix::safe_edit(Edit::insertion("()".to_string(), decorator.end()));
|
||||
pytest_mark_parentheses(checker, decorator, call_path, fix, "()", "");
|
||||
pytest_mark_parentheses(checker, decorator, marker, fix, "()", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_useless_usefixtures(checker: &mut Checker, decorator: &Decorator, call_path: &CallPath) {
|
||||
if *call_path.last().unwrap() != "usefixtures" {
|
||||
fn check_useless_usefixtures(checker: &mut Checker, decorator: &Decorator, marker: &str) {
|
||||
if marker != "usefixtures" {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut has_parameters = false;
|
||||
|
||||
if let Expr::Call(ast::ExprCall {
|
||||
arguments: Arguments { args, keywords, .. },
|
||||
..
|
||||
}) = &decorator.expression
|
||||
{
|
||||
if !args.is_empty() || !keywords.is_empty() {
|
||||
has_parameters = true;
|
||||
match &decorator.expression {
|
||||
// @pytest.mark.usefixtures
|
||||
Expr::Attribute(..) => {}
|
||||
// @pytest.mark.usefixtures(...)
|
||||
Expr::Call(ast::ExprCall {
|
||||
arguments: Arguments { args, keywords, .. },
|
||||
..
|
||||
}) => {
|
||||
if !args.is_empty() || !keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
if !has_parameters {
|
||||
let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(decorator.range())));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(decorator.range())));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
pub(crate) fn marks(checker: &mut Checker, decorators: &[Decorator]) {
|
||||
let enforce_parentheses = checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle);
|
||||
let enforce_useless_usefixtures = checker.enabled(Rule::PytestUseFixturesWithoutParameters);
|
||||
|
||||
for (decorator, call_path) in get_mark_decorators(decorators) {
|
||||
for (decorator, marker) in get_mark_decorators(decorators) {
|
||||
if enforce_parentheses {
|
||||
check_mark_parentheses(checker, decorator, &call_path);
|
||||
check_mark_parentheses(checker, decorator, marker);
|
||||
}
|
||||
if enforce_useless_usefixtures {
|
||||
check_useless_usefixtures(checker, decorator, &call_path);
|
||||
check_useless_usefixtures(checker, decorator, marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ use crate::settings::LinterSettings;
|
||||
/// ```python
|
||||
/// foo = "bar's"
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter automatically removes unnecessary escapes, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct AvoidableEscapedQuote;
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ use super::super::settings::Quote;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.inline-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent quotes for inline strings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesInlineString {
|
||||
preferred_quote: Quote,
|
||||
@@ -81,6 +88,13 @@ impl AlwaysFixableViolation for BadQuotesInlineString {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.multiline-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces double quotes for multiline strings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesMultilineString {
|
||||
preferred_quote: Quote,
|
||||
@@ -129,6 +143,13 @@ impl AlwaysFixableViolation for BadQuotesMultilineString {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.docstring-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces double quotes for docstrings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesDocstring {
|
||||
preferred_quote: Quote,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::BindingKind;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -15,6 +16,16 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Removing the parentheses makes the code more concise.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Parentheses can only be omitted if the exception is a class, as opposed to
|
||||
/// a function call. This rule isn't always capable of distinguishing between
|
||||
/// the two.
|
||||
///
|
||||
/// For example, if you import a function `module.get_exception` from another
|
||||
/// module, and `module.get_exception` returns an exception object, this rule will
|
||||
/// incorrectly mark the parentheses in `raise module.get_exception()` as
|
||||
/// unnecessary.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// raise TypeError()
|
||||
@@ -54,25 +65,32 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
|
||||
|
||||
if arguments.is_empty() {
|
||||
// `raise func()` still requires parentheses; only `raise Class()` does not.
|
||||
if checker
|
||||
.semantic()
|
||||
.lookup_attribute(func)
|
||||
.is_some_and(|id| checker.semantic().binding(id).kind.is_function_definition())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let exception_type = if let Some(id) = checker.semantic().lookup_attribute(func) {
|
||||
match checker.semantic().binding(id).kind {
|
||||
BindingKind::FunctionDefinition(_) => return,
|
||||
BindingKind::ClassDefinition(_) => Some(ExceptionType::Class),
|
||||
BindingKind::Builtin => Some(ExceptionType::Builtin),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
|
||||
// we might as well get it right.
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
|
||||
if exception_type
|
||||
.as_ref()
|
||||
.is_some_and(ExceptionType::is_builtin)
|
||||
&& checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, arguments.range());
|
||||
|
||||
// If the arguments are immediately followed by a `from`, insert whitespace to avoid
|
||||
// a syntax error, as in:
|
||||
// ```python
|
||||
@@ -85,13 +103,25 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
|
||||
.next()
|
||||
.is_some_and(char::is_alphanumeric)
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
" ".to_string(),
|
||||
arguments.range(),
|
||||
)));
|
||||
diagnostic.set_fix(if exception_type.is_some() {
|
||||
Fix::safe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
|
||||
} else {
|
||||
Fix::unsafe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
|
||||
});
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(arguments.range())));
|
||||
diagnostic.set_fix(if exception_type.is_some() {
|
||||
Fix::safe_edit(Edit::range_deletion(arguments.range()))
|
||||
} else {
|
||||
Fix::unsafe_edit(Edit::range_deletion(arguments.range()))
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, is_macro::Is)]
|
||||
enum ExceptionType {
|
||||
Class,
|
||||
Builtin,
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ RSE102.py:79:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
79 |+raise IndexError from ZeroDivisionError
|
||||
80 80 |
|
||||
81 81 | raise IndexError();
|
||||
82 82 |
|
||||
|
||||
RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
|
|
||||
@@ -245,6 +246,8 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
80 |
|
||||
81 | raise IndexError();
|
||||
| ^^ RSE102
|
||||
82 |
|
||||
83 | # RSE102
|
||||
|
|
||||
= help: Remove unnecessary parentheses
|
||||
|
||||
@@ -254,5 +257,23 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
80 80 |
|
||||
81 |-raise IndexError();
|
||||
81 |+raise IndexError;
|
||||
82 82 |
|
||||
83 83 | # RSE102
|
||||
84 84 | raise Foo()
|
||||
|
||||
RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
|
|
||||
83 | # RSE102
|
||||
84 | raise Foo()
|
||||
| ^^ RSE102
|
||||
|
|
||||
= help: Remove unnecessary parentheses
|
||||
|
||||
ℹ Suggested fix
|
||||
81 81 | raise IndexError();
|
||||
82 82 |
|
||||
83 83 | # RSE102
|
||||
84 |-raise Foo()
|
||||
84 |+raise Foo
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -53,4 +54,22 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_simplify").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ impl AlwaysFixableViolation for DictGetWithNoneDefault {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the given environment variable is allowed to be lowercase.
|
||||
///
|
||||
/// References:
|
||||
/// - <https://unix.stackexchange.com/a/212972/>
|
||||
/// - <https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy/>
|
||||
fn is_lowercase_allowed(env_var: &str) -> bool {
|
||||
matches!(env_var, "https_proxy" | "http_proxy" | "no_proxy")
|
||||
}
|
||||
|
||||
/// SIM112
|
||||
pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Expr) {
|
||||
// Ex) `os.environ['foo']`
|
||||
@@ -150,6 +159,10 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
|
||||
return;
|
||||
}
|
||||
|
||||
if is_lowercase_allowed(env_var) {
|
||||
return;
|
||||
}
|
||||
|
||||
let capital_env_var = env_var.to_ascii_uppercase();
|
||||
if &capital_env_var == env_var {
|
||||
return;
|
||||
@@ -194,6 +207,11 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if is_lowercase_allowed(env_var) {
|
||||
return;
|
||||
}
|
||||
|
||||
let capital_env_var = env_var.to_ascii_uppercase();
|
||||
if &capital_env_var == env_var {
|
||||
return;
|
||||
|
||||
@@ -139,7 +139,7 @@ pub(crate) fn multiple_with_statements(
|
||||
content,
|
||||
with_stmt.into(),
|
||||
checker.locator(),
|
||||
checker.settings.line_length,
|
||||
checker.settings.pycodestyle.max_line_length,
|
||||
checker.settings.tab_size,
|
||||
)
|
||||
}) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -96,6 +97,16 @@ pub(crate) fn nested_if_statements(
|
||||
return;
|
||||
};
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
|
||||
if is_type_checking_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CollapsibleIf,
|
||||
TextRange::new(nested_if.start(), colon.end()),
|
||||
@@ -117,7 +128,7 @@ pub(crate) fn nested_if_statements(
|
||||
content,
|
||||
(&nested_if).into(),
|
||||
checker.locator(),
|
||||
checker.settings.line_length,
|
||||
checker.settings.pycodestyle.max_line_length,
|
||||
checker.settings.tab_size,
|
||||
)
|
||||
}) {
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_python_ast::helpers::contains_effect;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, CmpOp, ElifElseClause, Expr, ExprContext, Identifier, Stmt,
|
||||
};
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -19,6 +20,9 @@ use crate::fix::edits::fits;
|
||||
/// the key is not found. When possible, using `dict.get` is more concise and
|
||||
/// more idiomatic.
|
||||
///
|
||||
/// Under [preview mode](https://docs.astral.sh/ruff/preview), this rule will
|
||||
/// also suggest replacing `if`-`else` _expressions_ with `dict.get` calls.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if "bar" in foo:
|
||||
@@ -32,6 +36,16 @@ use crate::fix::edits::fits;
|
||||
/// value = foo.get("bar", 0)
|
||||
/// ```
|
||||
///
|
||||
/// If preview mode is enabled:
|
||||
/// ```python
|
||||
/// value = foo["bar"] if "bar" in foo else 0
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// value = foo.get("bar", 0)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Mapping Types](https://docs.python.org/3/library/stdtypes.html#mapping-types-dict)
|
||||
#[violation]
|
||||
@@ -55,7 +69,7 @@ impl Violation for IfElseBlockInsteadOfDictGet {
|
||||
}
|
||||
|
||||
/// SIM401
|
||||
pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
@@ -136,6 +150,16 @@ pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::St
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
|
||||
if is_type_checking_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the default value is not "complex".
|
||||
if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) {
|
||||
return;
|
||||
@@ -171,7 +195,7 @@ pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::St
|
||||
&contents,
|
||||
stmt_if.into(),
|
||||
checker.locator(),
|
||||
checker.settings.line_length,
|
||||
checker.settings.pycodestyle.max_line_length,
|
||||
checker.settings.tab_size,
|
||||
) {
|
||||
return;
|
||||
@@ -191,3 +215,91 @@ pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::St
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// SIM401
|
||||
pub(crate) fn if_exp_instead_of_dict_get(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
test: &Expr,
|
||||
body: &Expr,
|
||||
orelse: &Expr,
|
||||
) {
|
||||
if checker.settings.preview.is_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left: test_key,
|
||||
ops,
|
||||
comparators: test_dict,
|
||||
range: _,
|
||||
}) = test
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [test_dict] = test_dict.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (body, default_value) = match ops.as_slice() {
|
||||
[CmpOp::In] => (body, orelse),
|
||||
[CmpOp::NotIn] => (orelse, body),
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Expr::Subscript(ast::ExprSubscript {
|
||||
value: expected_subscript,
|
||||
slice: expected_slice,
|
||||
..
|
||||
}) = body
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if ComparableExpr::from(expected_slice) != ComparableExpr::from(test_key)
|
||||
|| ComparableExpr::from(test_dict) != ComparableExpr::from(expected_subscript)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the default value is not "complex".
|
||||
if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let default_value_node = default_value.clone();
|
||||
let dict_key_node = *test_key.clone();
|
||||
let dict_get_node = ast::ExprAttribute {
|
||||
value: expected_subscript.clone(),
|
||||
attr: Identifier::new("get".to_string(), TextRange::default()),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let fixed_node = ast::ExprCall {
|
||||
func: Box::new(dict_get_node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![dict_key_node, default_value_node],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
};
|
||||
|
||||
let contents = checker.generator().expr(&fixed_node.into());
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
IfElseBlockInsteadOfDictGet {
|
||||
contents: contents.clone(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(expr, checker.locator()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableConstant;
|
||||
use ruff_python_ast::helpers::contains_effect;
|
||||
use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -39,7 +40,7 @@ impl Violation for IfElseBlockInsteadOfDictLookup {
|
||||
}
|
||||
}
|
||||
/// SIM116
|
||||
pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
// Throughout this rule:
|
||||
// * Each if or elif statement's test must consist of a constant equality check with the same variable.
|
||||
// * Each if or elif statement's body must consist of a single `return`.
|
||||
@@ -75,6 +76,7 @@ pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if value
|
||||
.as_ref()
|
||||
.is_some_and(|value| contains_effect(value, |id| checker.semantic().is_builtin(id)))
|
||||
@@ -82,6 +84,16 @@ pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
|
||||
if is_type_checking_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default();
|
||||
constants.insert(constant.into());
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::any_over_expr;
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -51,16 +50,14 @@ impl Violation for IfElseBlockInsteadOfIfExp {
|
||||
}
|
||||
|
||||
/// SIM108
|
||||
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
} = stmt_if;
|
||||
|
||||
// `test: None` to only match an `else` clause
|
||||
let [ElifElseClause {
|
||||
body: else_body,
|
||||
@@ -99,13 +96,6 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style and
|
||||
// `if sys.platform.startswith("...")`-style checks.
|
||||
let ignored_call_paths: &[&[&str]] = &[&["sys", "version_info"], &["sys", "platform"]];
|
||||
if contains_call_path(test, ignored_call_paths, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if (yield ...)`-style checks.
|
||||
// TODO(charlie): Fix precedence handling for yields in generator.
|
||||
if matches!(
|
||||
@@ -121,6 +111,16 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
|
||||
if is_type_checking_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target_var = &body_target;
|
||||
let ternary = ternary(target_var, body_value, test, else_value);
|
||||
let contents = checker.generator().stmt(&ternary);
|
||||
@@ -128,9 +128,9 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
if !fits(
|
||||
&contents,
|
||||
stmt.into(),
|
||||
stmt_if.into(),
|
||||
checker.locator(),
|
||||
checker.settings.line_length,
|
||||
checker.settings.pycodestyle.max_line_length,
|
||||
checker.settings.tab_size,
|
||||
) {
|
||||
return;
|
||||
@@ -140,26 +140,17 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
IfElseBlockInsteadOfIfExp {
|
||||
contents: contents.clone(),
|
||||
},
|
||||
stmt.range(),
|
||||
stmt_if.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(stmt, checker.locator()) {
|
||||
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
stmt.range(),
|
||||
stmt_if.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Return `true` if the `Expr` contains a reference to any of the given `${module}.${target}`.
|
||||
fn contains_call_path(expr: &Expr, targets: &[&[&str]], semantic: &SemanticModel) -> bool {
|
||||
any_over_expr(expr, &|expr| {
|
||||
semantic
|
||||
.resolve_call_path(expr)
|
||||
.is_some_and(|call_path| targets.iter().any(|target| &call_path.as_slice() == target))
|
||||
})
|
||||
}
|
||||
|
||||
fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Expr) -> Stmt {
|
||||
let node = ast::ExprIfExp {
|
||||
test: Box::new(test.clone()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Arguments, Constant, ElifElseClause, Expr, ExprContext, Stmt};
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,16 +49,14 @@ impl Violation for NeedlessBool {
|
||||
}
|
||||
|
||||
/// SIM103
|
||||
pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let ast::StmtIf {
|
||||
test: if_test,
|
||||
body: if_body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
} = stmt_if;
|
||||
|
||||
// Extract an `if` or `elif` (that returns) followed by an else (that returns the same value)
|
||||
let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() {
|
||||
// if-else case
|
||||
@@ -65,7 +64,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
body: else_body,
|
||||
test: None,
|
||||
..
|
||||
}] => (if_test.as_ref(), if_body, else_body, stmt.range()),
|
||||
}] => (if_test.as_ref(), if_body, else_body, stmt_if.range()),
|
||||
// elif-else case
|
||||
[.., ElifElseClause {
|
||||
body: elif_body,
|
||||
@@ -97,6 +96,16 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
|
||||
if is_type_checking_block(stmt_if, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let condition = checker.generator().expr(if_test);
|
||||
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
|
||||
if matches!(if_return, Bool::True)
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
|
||||
&contents,
|
||||
stmt.into(),
|
||||
checker.locator(),
|
||||
checker.settings.line_length,
|
||||
checker.settings.pycodestyle.max_line_length,
|
||||
checker.settings.tab_size,
|
||||
) {
|
||||
return;
|
||||
@@ -188,7 +188,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
|
||||
.slice(TextRange::new(line_start, stmt.start())),
|
||||
)
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
> checker.settings.pycodestyle.max_line_length
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM401.py:6:1: SIM401 [*] Use `var = a_dict.get(key, "default1")` instead of an `if` block
|
||||
|
|
||||
5 | # SIM401 (pattern-1)
|
||||
6 | / if key in a_dict:
|
||||
7 | | var = a_dict[key]
|
||||
8 | | else:
|
||||
9 | | var = "default1"
|
||||
| |____________________^ SIM401
|
||||
10 |
|
||||
11 | # SIM401 (pattern-2)
|
||||
|
|
||||
= help: Replace with `var = a_dict.get(key, "default1")`
|
||||
|
||||
ℹ Suggested fix
|
||||
3 3 | ###
|
||||
4 4 |
|
||||
5 5 | # SIM401 (pattern-1)
|
||||
6 |-if key in a_dict:
|
||||
7 |- var = a_dict[key]
|
||||
8 |-else:
|
||||
9 |- var = "default1"
|
||||
6 |+var = a_dict.get(key, "default1")
|
||||
10 7 |
|
||||
11 8 | # SIM401 (pattern-2)
|
||||
12 9 | if key not in a_dict:
|
||||
|
||||
SIM401.py:12:1: SIM401 [*] Use `var = a_dict.get(key, "default2")` instead of an `if` block
|
||||
|
|
||||
11 | # SIM401 (pattern-2)
|
||||
12 | / if key not in a_dict:
|
||||
13 | | var = "default2"
|
||||
14 | | else:
|
||||
15 | | var = a_dict[key]
|
||||
| |_____________________^ SIM401
|
||||
16 |
|
||||
17 | # OK (default contains effect)
|
||||
|
|
||||
= help: Replace with `var = a_dict.get(key, "default2")`
|
||||
|
||||
ℹ Suggested fix
|
||||
9 9 | var = "default1"
|
||||
10 10 |
|
||||
11 11 | # SIM401 (pattern-2)
|
||||
12 |-if key not in a_dict:
|
||||
13 |- var = "default2"
|
||||
14 |-else:
|
||||
15 |- var = a_dict[key]
|
||||
12 |+var = a_dict.get(key, "default2")
|
||||
16 13 |
|
||||
17 14 | # OK (default contains effect)
|
||||
18 15 | if key in a_dict:
|
||||
|
||||
SIM401.py:24:1: SIM401 [*] Use `var = a_dict.get(keys[idx], "default")` instead of an `if` block
|
||||
|
|
||||
23 | # SIM401 (complex expression in key)
|
||||
24 | / if keys[idx] in a_dict:
|
||||
25 | | var = a_dict[keys[idx]]
|
||||
26 | | else:
|
||||
27 | | var = "default"
|
||||
| |___________________^ SIM401
|
||||
28 |
|
||||
29 | # SIM401 (complex expression in dict)
|
||||
|
|
||||
= help: Replace with `var = a_dict.get(keys[idx], "default")`
|
||||
|
||||
ℹ Suggested fix
|
||||
21 21 | var = val1 + val2
|
||||
22 22 |
|
||||
23 23 | # SIM401 (complex expression in key)
|
||||
24 |-if keys[idx] in a_dict:
|
||||
25 |- var = a_dict[keys[idx]]
|
||||
26 |-else:
|
||||
27 |- var = "default"
|
||||
24 |+var = a_dict.get(keys[idx], "default")
|
||||
28 25 |
|
||||
29 26 | # SIM401 (complex expression in dict)
|
||||
30 27 | if key in dicts[idx]:
|
||||
|
||||
SIM401.py:30:1: SIM401 [*] Use `var = dicts[idx].get(key, "default")` instead of an `if` block
|
||||
|
|
||||
29 | # SIM401 (complex expression in dict)
|
||||
30 | / if key in dicts[idx]:
|
||||
31 | | var = dicts[idx][key]
|
||||
32 | | else:
|
||||
33 | | var = "default"
|
||||
| |___________________^ SIM401
|
||||
34 |
|
||||
35 | # SIM401 (complex expression in var)
|
||||
|
|
||||
= help: Replace with `var = dicts[idx].get(key, "default")`
|
||||
|
||||
ℹ Suggested fix
|
||||
27 27 | var = "default"
|
||||
28 28 |
|
||||
29 29 | # SIM401 (complex expression in dict)
|
||||
30 |-if key in dicts[idx]:
|
||||
31 |- var = dicts[idx][key]
|
||||
32 |-else:
|
||||
33 |- var = "default"
|
||||
30 |+var = dicts[idx].get(key, "default")
|
||||
34 31 |
|
||||
35 32 | # SIM401 (complex expression in var)
|
||||
36 33 | if key in a_dict:
|
||||
|
||||
SIM401.py:36:1: SIM401 [*] Use `vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789")` instead of an `if` block
|
||||
|
|
||||
35 | # SIM401 (complex expression in var)
|
||||
36 | / if key in a_dict:
|
||||
37 | | vars[idx] = a_dict[key]
|
||||
38 | | else:
|
||||
39 | | vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
| |___________________________________________________________________________^ SIM401
|
||||
40 |
|
||||
41 | # SIM401
|
||||
|
|
||||
= help: Replace with `vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789")`
|
||||
|
||||
ℹ Suggested fix
|
||||
33 33 | var = "default"
|
||||
34 34 |
|
||||
35 35 | # SIM401 (complex expression in var)
|
||||
36 |-if key in a_dict:
|
||||
37 |- vars[idx] = a_dict[key]
|
||||
38 |-else:
|
||||
39 |- vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
36 |+vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789")
|
||||
40 37 |
|
||||
41 38 | # SIM401
|
||||
42 39 | if foo():
|
||||
|
||||
SIM401.py:45:5: SIM401 [*] Use `vars[idx] = a_dict.get(key, "default")` instead of an `if` block
|
||||
|
|
||||
43 | pass
|
||||
44 | else:
|
||||
45 | if key in a_dict:
|
||||
| _____^
|
||||
46 | | vars[idx] = a_dict[key]
|
||||
47 | | else:
|
||||
48 | | vars[idx] = "default"
|
||||
| |_____________________________^ SIM401
|
||||
49 |
|
||||
50 | ###
|
||||
|
|
||||
= help: Replace with `vars[idx] = a_dict.get(key, "default")`
|
||||
|
||||
ℹ Suggested fix
|
||||
42 42 | if foo():
|
||||
43 43 | pass
|
||||
44 44 | else:
|
||||
45 |- if key in a_dict:
|
||||
46 |- vars[idx] = a_dict[key]
|
||||
47 |- else:
|
||||
48 |- vars[idx] = "default"
|
||||
45 |+ vars[idx] = a_dict.get(key, "default")
|
||||
49 46 |
|
||||
50 47 | ###
|
||||
51 48 | # Negative cases
|
||||
|
||||
SIM401.py:123:7: SIM401 [*] Use `a_dict.get(key, "default3")` instead of an `if` block
|
||||
|
|
||||
122 | # SIM401
|
||||
123 | var = a_dict[key] if key in a_dict else "default3"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM401
|
||||
124 |
|
||||
125 | # SIM401
|
||||
|
|
||||
= help: Replace with `a_dict.get(key, "default3")`
|
||||
|
||||
ℹ Suggested fix
|
||||
120 120 | ###
|
||||
121 121 |
|
||||
122 122 | # SIM401
|
||||
123 |-var = a_dict[key] if key in a_dict else "default3"
|
||||
123 |+var = a_dict.get(key, "default3")
|
||||
124 124 |
|
||||
125 125 | # SIM401
|
||||
126 126 | var = "default-1" if key not in a_dict else a_dict[key]
|
||||
|
||||
SIM401.py:126:7: SIM401 [*] Use `a_dict.get(key, "default-1")` instead of an `if` block
|
||||
|
|
||||
125 | # SIM401
|
||||
126 | var = "default-1" if key not in a_dict else a_dict[key]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM401
|
||||
127 |
|
||||
128 | # OK (default contains effect)
|
||||
|
|
||||
= help: Replace with `a_dict.get(key, "default-1")`
|
||||
|
||||
ℹ Suggested fix
|
||||
123 123 | var = a_dict[key] if key in a_dict else "default3"
|
||||
124 124 |
|
||||
125 125 | # SIM401
|
||||
126 |-var = "default-1" if key not in a_dict else a_dict[key]
|
||||
126 |+var = a_dict.get(key, "default-1")
|
||||
127 127 |
|
||||
128 128 | # OK (default contains effect)
|
||||
129 129 | var = a_dict[key] if key in a_dict else val1 + val2
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ mod tests {
|
||||
|
||||
use crate::line_width::LineLength;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pycodestyle;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -61,6 +63,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pycodestyle").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn w292_4() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
@@ -210,7 +230,10 @@ mod tests {
|
||||
Path::new("pycodestyle/E501_2.py"),
|
||||
&settings::LinterSettings {
|
||||
tab_size: NonZeroU8::new(tab_size).unwrap().into(),
|
||||
line_length: LineLength::try_from(6).unwrap(),
|
||||
pycodestyle: pycodestyle::settings::Settings {
|
||||
max_line_length: LineLength::try_from(6).unwrap(),
|
||||
..pycodestyle::settings::Settings::default()
|
||||
},
|
||||
..settings::LinterSettings::for_rule(Rule::LineTooLong)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use ruff_python_trivia::is_pragma_comment;
|
||||
use ruff_source_file::Line;
|
||||
use ruff_text_size::{TextLen, TextRange};
|
||||
|
||||
use crate::line_width::{LineLength, LineWidthBuilder, TabSize};
|
||||
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Overlong {
|
||||
@@ -23,7 +23,7 @@ impl Overlong {
|
||||
indexer: &Indexer,
|
||||
limit: LineLength,
|
||||
task_tags: &[String],
|
||||
tab_size: TabSize,
|
||||
tab_size: IndentWidth,
|
||||
) -> Option<Self> {
|
||||
// The maximum width of the line is the number of bytes multiplied by the tab size (the
|
||||
// worst-case scenario is that the line is all tabs). If the maximum width is less than the
|
||||
@@ -158,7 +158,7 @@ impl<'a> Deref for StrippedLine<'a> {
|
||||
}
|
||||
|
||||
/// Returns the width of a given string, accounting for the tab size.
|
||||
fn measure(s: &str, tab_size: TabSize) -> LineWidthBuilder {
|
||||
fn measure(s: &str, tab_size: IndentWidth) -> LineWidthBuilder {
|
||||
let mut width = LineWidthBuilder::new(tab_size);
|
||||
width = width.add_str(s);
|
||||
width
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Violation for DocLineTooLong {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DocLineTooLong(width, limit) = self;
|
||||
format!("Doc line too long ({width} > {limit} characters)")
|
||||
format!("Doc line too long ({width} > {limit})")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::fix::edits::pad_start;
|
||||
|
||||
@@ -25,23 +25,51 @@ use crate::fix::edits::pad_start;
|
||||
/// regex = r"\.png$"
|
||||
/// ```
|
||||
///
|
||||
/// Or, if the string already contains a valid escape sequence:
|
||||
/// ```python
|
||||
/// value = "new line\nand invalid escape \_ here"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// value = "new line\nand invalid escape \\_ here"
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals)
|
||||
#[violation]
|
||||
pub struct InvalidEscapeSequence(char);
|
||||
pub struct InvalidEscapeSequence {
|
||||
ch: char,
|
||||
fix_title: FixTitle,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for InvalidEscapeSequence {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let InvalidEscapeSequence(char) = self;
|
||||
format!("Invalid escape sequence: `\\{char}`")
|
||||
let InvalidEscapeSequence { ch, .. } = self;
|
||||
format!("Invalid escape sequence: `\\{ch}`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add backslash to escape sequence".to_string()
|
||||
match self.fix_title {
|
||||
FixTitle::AddBackslash => format!("Add backslash to escape sequence"),
|
||||
FixTitle::UseRawStringLiteral => format!("Use a raw string literal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum FixTitle {
|
||||
AddBackslash,
|
||||
UseRawStringLiteral,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InvalidEscapeChar {
|
||||
ch: char,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
/// W605
|
||||
pub(crate) fn invalid_escape_sequence(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
@@ -67,7 +95,7 @@ pub(crate) fn invalid_escape_sequence(
|
||||
};
|
||||
|
||||
let mut contains_valid_escape_sequence = false;
|
||||
let mut invalid_escape_sequence = Vec::new();
|
||||
let mut invalid_escape_chars = Vec::new();
|
||||
|
||||
let mut prev = None;
|
||||
let bytes = token_source_code.as_bytes();
|
||||
@@ -154,16 +182,28 @@ pub(crate) fn invalid_escape_sequence(
|
||||
|
||||
let location = token_range.start() + TextSize::try_from(i).unwrap();
|
||||
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
|
||||
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
|
||||
invalid_escape_chars.push(InvalidEscapeChar {
|
||||
ch: next_char,
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
let mut invalid_escape_sequence = Vec::new();
|
||||
if contains_valid_escape_sequence {
|
||||
// Escape with backslash.
|
||||
for diagnostic in &mut invalid_escape_sequence {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
for invalid_escape_char in &invalid_escape_chars {
|
||||
let diagnostic = Diagnostic::new(
|
||||
InvalidEscapeSequence {
|
||||
ch: invalid_escape_char.ch,
|
||||
fix_title: FixTitle::AddBackslash,
|
||||
},
|
||||
invalid_escape_char.range,
|
||||
)
|
||||
.with_fix(Fix::safe_edit(Edit::insertion(
|
||||
r"\".to_string(),
|
||||
diagnostic.start() + TextSize::from(1),
|
||||
invalid_escape_char.range.start() + TextSize::from(1),
|
||||
)));
|
||||
invalid_escape_sequence.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
let tok_start = if token.is_f_string_middle() {
|
||||
@@ -178,14 +218,24 @@ pub(crate) fn invalid_escape_sequence(
|
||||
token_range.start()
|
||||
};
|
||||
// Turn into raw string.
|
||||
for diagnostic in &mut invalid_escape_sequence {
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||
// `returnr"foo"` is not.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
pad_start("r".to_string(), tok_start, locator),
|
||||
tok_start,
|
||||
)));
|
||||
for invalid_escape_char in &invalid_escape_chars {
|
||||
let diagnostic = Diagnostic::new(
|
||||
InvalidEscapeSequence {
|
||||
ch: invalid_escape_char.ch,
|
||||
fix_title: FixTitle::UseRawStringLiteral,
|
||||
},
|
||||
invalid_escape_char.range,
|
||||
)
|
||||
.with_fix(
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||
// `returnr"foo"` is not.
|
||||
Fix::safe_edit(Edit::insertion(
|
||||
pad_start("r".to_string(), tok_start, locator),
|
||||
tok_start,
|
||||
)),
|
||||
);
|
||||
invalid_escape_sequence.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ use crate::settings::LinterSettings;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `line-length`
|
||||
/// - `pycodestyle.max-line-length`
|
||||
/// - `task-tags`
|
||||
/// - `pycodestyle.ignore-overlong-task-comments`
|
||||
///
|
||||
@@ -58,7 +59,7 @@ impl Violation for LineTooLong {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let LineTooLong(width, limit) = self;
|
||||
format!("Line too long ({width} > {limit} characters)")
|
||||
format!("Line too long ({width} > {limit})")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ pub(crate) fn line_too_long(
|
||||
indexer: &Indexer,
|
||||
settings: &LinterSettings,
|
||||
) -> Option<Diagnostic> {
|
||||
let limit = settings.line_length;
|
||||
let limit = settings.pycodestyle.max_line_length;
|
||||
|
||||
Overlong::try_from_line(
|
||||
line,
|
||||
|
||||
@@ -23,7 +23,16 @@ use super::LogicalLine;
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `indent-width` with a value other than `4`.
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
|
||||
/// [formatter]:https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct IndentationWithInvalidMultiple {
|
||||
indent_size: usize,
|
||||
@@ -55,7 +64,15 @@ impl Violation for IndentationWithInvalidMultiple {
|
||||
/// # a = 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `indent-width` with a value other than `4`.
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
|
||||
/// [formatter]:https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct IndentationWithInvalidMultipleComment {
|
||||
indent_size: usize,
|
||||
|
||||
@@ -47,8 +47,7 @@ impl AlwaysFixableViolation for MissingWhitespace {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let token = self.token_text();
|
||||
format!("Added missing whitespace after '{token}'")
|
||||
format!("Add missing whitespace")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -29,11 +29,15 @@ use crate::rules::pycodestyle::rules::logical_lines::LogicalLine;
|
||||
#[violation]
|
||||
pub struct MissingWhitespaceAfterKeyword;
|
||||
|
||||
impl Violation for MissingWhitespaceAfterKeyword {
|
||||
impl AlwaysFixableViolation for MissingWhitespaceAfterKeyword {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing whitespace after keyword")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Added missing whitespace after keyword")
|
||||
}
|
||||
}
|
||||
|
||||
/// E275
|
||||
@@ -59,7 +63,9 @@ pub(crate) fn missing_whitespace_after_keyword(
|
||||
))
|
||||
&& tok0.end() == tok1.start()
|
||||
{
|
||||
context.push(MissingWhitespaceAfterKeyword, tok0.range());
|
||||
let mut diagnostic = Diagnostic::new(MissingWhitespaceAfterKeyword, tok0.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(" ".to_string(), tok0.end())));
|
||||
context.push_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user