Compare commits
88 Commits
zb/pygrep
...
charlie/to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0afde1a3f | ||
|
|
df7fb95cbc | ||
|
|
83195a6030 | ||
|
|
d31d09d7cd | ||
|
|
0f436b71f3 | ||
|
|
cd5bcd815d | ||
|
|
0ccca4083a | ||
|
|
041ce1e166 | ||
|
|
36b752876e | ||
|
|
b3dc565473 | ||
|
|
e708c08b64 | ||
|
|
73902323d5 | ||
|
|
9781563ef6 | ||
|
|
84aea7f0c8 | ||
|
|
b47f85eb69 | ||
|
|
80fc02e7d5 | ||
|
|
55d0e1148c | ||
|
|
1de945e3eb | ||
|
|
e277ba20da | ||
|
|
2e836a4cbe | ||
|
|
57d6cdb8d3 | ||
|
|
602f8b8250 | ||
|
|
a6bc4b2e48 | ||
|
|
c5fa0ccffb | ||
|
|
dd77d29d0e | ||
|
|
ad0121660e | ||
|
|
5c99967c4d | ||
|
|
c53aae0b6f | ||
|
|
2352de2277 | ||
|
|
e0a6034cbb | ||
|
|
25d93053da | ||
|
|
ee5b07d4ca | ||
|
|
e50603caf6 | ||
|
|
c3ca34543f | ||
|
|
4f7fb566f0 | ||
|
|
50bfbcf568 | ||
|
|
ea1c089652 | ||
|
|
b947dde8ad | ||
|
|
d259cd0d32 | ||
|
|
af4db39205 | ||
|
|
467c091382 | ||
|
|
92d99a72d9 | ||
|
|
66d2c1e1c4 | ||
|
|
ded8c7629f | ||
|
|
1fadefa67b | ||
|
|
06ad687efd | ||
|
|
148b64ead3 | ||
|
|
99eddbd2a0 | ||
|
|
836d2eaa01 | ||
|
|
994514d686 | ||
|
|
a578414246 | ||
|
|
0d752e56cd | ||
|
|
46c0937bfa | ||
|
|
e5008ca714 | ||
|
|
85a7edcc70 | ||
|
|
7db3aea1c6 | ||
|
|
e0bc08a758 | ||
|
|
a0ef087e73 | ||
|
|
c86e14d1d4 | ||
|
|
e37b3b0742 | ||
|
|
a0f32dfa55 | ||
|
|
6aa643346f | ||
|
|
ae13d8fddf | ||
|
|
2d6fd0fc91 | ||
|
|
33fe988cfc | ||
|
|
0f674d1d90 | ||
|
|
7962bca40a | ||
|
|
b81fc5ed11 | ||
|
|
c2bf725086 | ||
|
|
c3b33e9c4d | ||
|
|
6996ff7b1e | ||
|
|
f18e7d40ac | ||
|
|
2cc8acb0b7 | ||
|
|
ad83944ded | ||
|
|
6e225cb57c | ||
|
|
7992583908 | ||
|
|
1a46c9c2a2 | ||
|
|
3f4ab87061 | ||
|
|
7ae7bf6e30 | ||
|
|
9e3ff01ce8 | ||
|
|
63a7de9018 | ||
|
|
b77021b9ed | ||
|
|
f0066e1b89 | ||
|
|
7642fb7f27 | ||
|
|
7a1fa0e5d8 | ||
|
|
ce14f4dea5 | ||
|
|
6bb126415d | ||
|
|
541aef4e6c |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -9,6 +9,10 @@ updates:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
ignore:
|
||||
# The latest versions of these are not compatible with our release workflow
|
||||
- dependency-name: "actions/upload-artifact"
|
||||
- dependency-name: "actions/download-artifact"
|
||||
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
|
||||
11
.github/workflows/ci.yaml
vendored
11
.github/workflows/ci.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: tj-actions/changed-files@v41
|
||||
- uses: tj-actions/changed-files@v42
|
||||
id: changed
|
||||
with:
|
||||
files_yaml: |
|
||||
@@ -117,7 +117,10 @@ jobs:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --all --all-features --unreferenced reject
|
||||
run: cargo insta test --all --exclude ruff_dev --all-features --unreferenced reject
|
||||
- name: "Run dev tests"
|
||||
# e.g. generating the schema — these should not run with all features enabled
|
||||
run: cargo insta test -p ruff_dev --unreferenced reject
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
@@ -146,7 +149,7 @@ jobs:
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
# We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
|
||||
run: cargo insta test --all --all-features
|
||||
run: cargo insta test --all --exclude ruff_dev --all-features
|
||||
|
||||
cargo-test-wasm:
|
||||
name: "cargo test (wasm)"
|
||||
@@ -382,7 +385,7 @@ jobs:
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
uses: peter-evans/find-comment@v3
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
|
||||
210
CHANGELOG.md
210
CHANGELOG.md
@@ -1,5 +1,215 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.1
|
||||
|
||||
This release includes support for range formatting (i.e., the ability to format specific lines
|
||||
within a source file).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
|
||||
- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Add `--range` option to `ruff format` ([#9733](https://github.com/astral-sh/ruff/pull/9733))
|
||||
- Don't trim last empty line in docstrings ([#9813](https://github.com/astral-sh/ruff/pull/9813))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Skip empty lines when determining base indentation ([#9795](https://github.com/astral-sh/ruff/pull/9795))
|
||||
- Drop `__get__` and `__set__` from `unnecessary-dunder-call` ([#9791](https://github.com/astral-sh/ruff/pull/9791))
|
||||
- Respect generic `Protocol` in ellipsis removal ([#9841](https://github.com/astral-sh/ruff/pull/9841))
|
||||
- Revert "Use publicly available Apple Silicon runners (#9726)" ([#9834](https://github.com/astral-sh/ruff/pull/9834))
|
||||
|
||||
### Performance
|
||||
|
||||
- Skip LibCST parsing for standard dedent adjustments ([#9769](https://github.com/astral-sh/ruff/pull/9769))
|
||||
- Remove CST-based fixer for `C408` ([#9822](https://github.com/astral-sh/ruff/pull/9822))
|
||||
- Add our own ignored-names abstractions ([#9802](https://github.com/astral-sh/ruff/pull/9802))
|
||||
- Remove CST-based fixers for `C400`, `C401`, `C410`, and `C418` ([#9819](https://github.com/astral-sh/ruff/pull/9819))
|
||||
- Use `AhoCorasick` to speed up quote match ([#9773](https://github.com/astral-sh/ruff/pull/9773))
|
||||
- Remove CST-based fixers for `C405` and `C409` ([#9821](https://github.com/astral-sh/ruff/pull/9821))
|
||||
- Add fast-path for comment detection ([#9808](https://github.com/astral-sh/ruff/pull/9808))
|
||||
- Invert order of checks in `zero-sleep-call` ([#9766](https://github.com/astral-sh/ruff/pull/9766))
|
||||
- Short-circuit typing matches based on imports ([#9800](https://github.com/astral-sh/ruff/pull/9800))
|
||||
- Run dunder method rule on methods directly ([#9815](https://github.com/astral-sh/ruff/pull/9815))
|
||||
- Track top-level module imports in the semantic model ([#9775](https://github.com/astral-sh/ruff/pull/9775))
|
||||
- Slight speed-up for lowercase and uppercase identifier checks ([#9798](https://github.com/astral-sh/ruff/pull/9798))
|
||||
- Remove LibCST-based fixer for `C403` ([#9818](https://github.com/astral-sh/ruff/pull/9818))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update `max-pos-args` example to `max-positional-args` ([#9797](https://github.com/astral-sh/ruff/pull/9797))
|
||||
- Fixed example code in `weak_cryptographic_key.rs` ([#9774](https://github.com/astral-sh/ruff/pull/9774))
|
||||
- Fix references to deprecated `ANN` rules in changelog ([#9771](https://github.com/astral-sh/ruff/pull/9771))
|
||||
- Fix default for `max-positional-args` ([#9838](https://github.com/astral-sh/ruff/pull/9838))
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The `NURSERY` selector cannot be used anymore
|
||||
- Legacy selection of nursery rules by exact codes is no longer allowed without preview enabled
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
### Deprecations
|
||||
|
||||
The following rules are now deprecated:
|
||||
|
||||
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
|
||||
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
|
||||
|
||||
The following command line options are now deprecated:
|
||||
|
||||
- `--show-source`; use `--output-format full` instead
|
||||
- `--no-show-source`; use `--output-format concise` instead
|
||||
- `--output-format text`; use `full` or `concise` instead
|
||||
|
||||
The following settings have moved and the previous name is deprecated:
|
||||
|
||||
- `ruff.allowed-confusables` → [`ruff.lint.allowed-confusables`](https://docs.astral.sh//ruff/settings/#lint_allowed-confusables)
|
||||
- `ruff.dummy-variable-rgx` → [`ruff.lint.dummy-variable-rgx`](https://docs.astral.sh//ruff/settings/#lint_dummy-variable-rgx)
|
||||
- `ruff.explicit-preview-rules` → [`ruff.lint.explicit-preview-rules`](https://docs.astral.sh//ruff/settings/#lint_explicit-preview-rules)
|
||||
- `ruff.extend-fixable` → [`ruff.lint.extend-fixable`](https://docs.astral.sh//ruff/settings/#lint_extend-fixable)
|
||||
- `ruff.extend-ignore` → [`ruff.lint.extend-ignore`](https://docs.astral.sh//ruff/settings/#lint_extend-ignore)
|
||||
- `ruff.extend-per-file-ignores` → [`ruff.lint.extend-per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_extend-per-file-ignores)
|
||||
- `ruff.extend-safe-fixes` → [`ruff.lint.extend-safe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-safe-fixes)
|
||||
- `ruff.extend-select` → [`ruff.lint.extend-select`](https://docs.astral.sh//ruff/settings/#lint_extend-select)
|
||||
- `ruff.extend-unfixable` → [`ruff.lint.extend-unfixable`](https://docs.astral.sh//ruff/settings/#lint_extend-unfixable)
|
||||
- `ruff.extend-unsafe-fixes` → [`ruff.lint.extend-unsafe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-unsafe-fixes)
|
||||
- `ruff.external` → [`ruff.lint.external`](https://docs.astral.sh//ruff/settings/#lint_external)
|
||||
- `ruff.fixable` → [`ruff.lint.fixable`](https://docs.astral.sh//ruff/settings/#lint_fixable)
|
||||
- `ruff.flake8-annotations` → [`ruff.lint.flake8-annotations`](https://docs.astral.sh//ruff/settings/#lint_flake8-annotations)
|
||||
- `ruff.flake8-bandit` → [`ruff.lint.flake8-bandit`](https://docs.astral.sh//ruff/settings/#lint_flake8-bandit)
|
||||
- `ruff.flake8-bugbear` → [`ruff.lint.flake8-bugbear`](https://docs.astral.sh//ruff/settings/#lint_flake8-bugbear)
|
||||
- `ruff.flake8-builtins` → [`ruff.lint.flake8-builtins`](https://docs.astral.sh//ruff/settings/#lint_flake8-builtins)
|
||||
- `ruff.flake8-comprehensions` → [`ruff.lint.flake8-comprehensions`](https://docs.astral.sh//ruff/settings/#lint_flake8-comprehensions)
|
||||
- `ruff.flake8-copyright` → [`ruff.lint.flake8-copyright`](https://docs.astral.sh//ruff/settings/#lint_flake8-copyright)
|
||||
- `ruff.flake8-errmsg` → [`ruff.lint.flake8-errmsg`](https://docs.astral.sh//ruff/settings/#lint_flake8-errmsg)
|
||||
- `ruff.flake8-gettext` → [`ruff.lint.flake8-gettext`](https://docs.astral.sh//ruff/settings/#lint_flake8-gettext)
|
||||
- `ruff.flake8-implicit-str-concat` → [`ruff.lint.flake8-implicit-str-concat`](https://docs.astral.sh//ruff/settings/#lint_flake8-implicit-str-concat)
|
||||
- `ruff.flake8-import-conventions` → [`ruff.lint.flake8-import-conventions`](https://docs.astral.sh//ruff/settings/#lint_flake8-import-conventions)
|
||||
- `ruff.flake8-pytest-style` → [`ruff.lint.flake8-pytest-style`](https://docs.astral.sh//ruff/settings/#lint_flake8-pytest-style)
|
||||
- `ruff.flake8-quotes` → [`ruff.lint.flake8-quotes`](https://docs.astral.sh//ruff/settings/#lint_flake8-quotes)
|
||||
- `ruff.flake8-self` → [`ruff.lint.flake8-self`](https://docs.astral.sh//ruff/settings/#lint_flake8-self)
|
||||
- `ruff.flake8-tidy-imports` → [`ruff.lint.flake8-tidy-imports`](https://docs.astral.sh//ruff/settings/#lint_flake8-tidy-imports)
|
||||
- `ruff.flake8-type-checking` → [`ruff.lint.flake8-type-checking`](https://docs.astral.sh//ruff/settings/#lint_flake8-type-checking)
|
||||
- `ruff.flake8-unused-arguments` → [`ruff.lint.flake8-unused-arguments`](https://docs.astral.sh//ruff/settings/#lint_flake8-unused-arguments)
|
||||
- `ruff.ignore` → [`ruff.lint.ignore`](https://docs.astral.sh//ruff/settings/#lint_ignore)
|
||||
- `ruff.ignore-init-module-imports` → [`ruff.lint.ignore-init-module-imports`](https://docs.astral.sh//ruff/settings/#lint_ignore-init-module-imports)
|
||||
- `ruff.isort` → [`ruff.lint.isort`](https://docs.astral.sh//ruff/settings/#lint_isort)
|
||||
- `ruff.logger-objects` → [`ruff.lint.logger-objects`](https://docs.astral.sh//ruff/settings/#lint_logger-objects)
|
||||
- `ruff.mccabe` → [`ruff.lint.mccabe`](https://docs.astral.sh//ruff/settings/#lint_mccabe)
|
||||
- `ruff.pep8-naming` → [`ruff.lint.pep8-naming`](https://docs.astral.sh//ruff/settings/#lint_pep8-naming)
|
||||
- `ruff.per-file-ignores` → [`ruff.lint.per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_per-file-ignores)
|
||||
- `ruff.pycodestyle` → [`ruff.lint.pycodestyle`](https://docs.astral.sh//ruff/settings/#lint_pycodestyle)
|
||||
- `ruff.pydocstyle` → [`ruff.lint.pydocstyle`](https://docs.astral.sh//ruff/settings/#lint_pydocstyle)
|
||||
- `ruff.pyflakes` → [`ruff.lint.pyflakes`](https://docs.astral.sh//ruff/settings/#lint_pyflakes)
|
||||
- `ruff.pylint` → [`ruff.lint.pylint`](https://docs.astral.sh//ruff/settings/#lint_pylint)
|
||||
- `ruff.pyupgrade` → [`ruff.lint.pyupgrade`](https://docs.astral.sh//ruff/settings/#lint_pyupgrade)
|
||||
- `ruff.select` → [`ruff.lint.select`](https://docs.astral.sh//ruff/settings/#lint_select)
|
||||
- `ruff.task-tags` → [`ruff.lint.task-tags`](https://docs.astral.sh//ruff/settings/#lint_task-tags)
|
||||
- `ruff.typing-modules` → [`ruff.lint.typing-modules`](https://docs.astral.sh//ruff/settings/#lint_typing-modules)
|
||||
- `ruff.unfixable` → [`ruff.lint.unfixable`](https://docs.astral.sh//ruff/settings/#lint_unfixable)
|
||||
|
||||
### Remapped rules
|
||||
|
||||
The following rules have been remapped to new codes:
|
||||
|
||||
- [`raise-without-from-inside-except`](https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/): `TRY200` to `B904`
|
||||
- [`suspicious-eval-usage`](https://docs.astral.sh/ruff/rules/suspicious-eval-usage/): `PGH001` to `S307`
|
||||
- [`logging-warn`](https://docs.astral.sh/ruff/rules/logging-warn/): `PGH002` to `G010`
|
||||
- [`static-key-dict-comprehension`](https://docs.astral.sh/ruff/rules/static-key-dict-comprehension): `RUF011` to `B035`
|
||||
- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union): `TCH006` to `TCH010`
|
||||
|
||||
### Stabilizations
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await) (`TRIO100`)
|
||||
- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call) (`TRIO105`)
|
||||
- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout) (`TRIO109`)
|
||||
- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep) (`TRIO110`)
|
||||
- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call) (`TRIO115`)
|
||||
- [`unnecessary-escaped-quote`](https://docs.astral.sh/ruff/rules/unnecessary-escaped-quote) (`Q004`)
|
||||
- [`enumerate-for-loop`](https://docs.astral.sh/ruff/rules/enumerate-for-loop) (`SIM113`)
|
||||
- [`zip-dict-keys-and-values`](https://docs.astral.sh/ruff/rules/zip-dict-keys-and-values) (`SIM911`)
|
||||
- [`timeout-error-alias`](https://docs.astral.sh/ruff/rules/timeout-error-alias) (`UP041`)
|
||||
- [`flask-debug-true`](https://docs.astral.sh/ruff/rules/flask-debug-true) (`S201`)
|
||||
- [`tarfile-unsafe-members`](https://docs.astral.sh/ruff/rules/tarfile-unsafe-members) (`S202`)
|
||||
- [`ssl-insecure-version`](https://docs.astral.sh/ruff/rules/ssl-insecure-version) (`S502`)
|
||||
- [`ssl-with-bad-defaults`](https://docs.astral.sh/ruff/rules/ssl-with-bad-defaults) (`S503`)
|
||||
- [`ssl-with-no-version`](https://docs.astral.sh/ruff/rules/ssl-with-no-version) (`S504`)
|
||||
- [`weak-cryptographic-key`](https://docs.astral.sh/ruff/rules/weak-cryptographic-key) (`S505`)
|
||||
- [`ssh-no-host-key-verification`](https://docs.astral.sh/ruff/rules/ssh-no-host-key-verification) (`S507`)
|
||||
- [`django-raw-sql`](https://docs.astral.sh/ruff/rules/django-raw-sql) (`S611`)
|
||||
- [`mako-templates`](https://docs.astral.sh/ruff/rules/mako-templates) (`S702`)
|
||||
- [`generator-return-from-iter-method`](https://docs.astral.sh/ruff/rules/generator-return-from-iter-method) (`PYI058`)
|
||||
- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union) (`TCH006`)
|
||||
- [`numpy2-deprecation`](https://docs.astral.sh/ruff/rules/numpy2-deprecation) (`NPY201`)
|
||||
- [`quadratic-list-summation`](https://docs.astral.sh/ruff/rules/quadratic-list-summation) (`RUF017`)
|
||||
- [`assignment-in-assert`](https://docs.astral.sh/ruff/rules/assignment-in-assert) (`RUF018`)
|
||||
- [`unnecessary-key-check`](https://docs.astral.sh/ruff/rules/unnecessary-key-check) (`RUF019`)
|
||||
- [`never-union`](https://docs.astral.sh/ruff/rules/never-union) (`RUF020`)
|
||||
- [`direct-logger-instantiation`](https://docs.astral.sh/ruff/rules/direct-logger-instantiation) (`LOG001`)
|
||||
- [`invalid-get-logger-argument`](https://docs.astral.sh/ruff/rules/invalid-get-logger-argument) (`LOG002`)
|
||||
- [`exception-without-exc-info`](https://docs.astral.sh/ruff/rules/exception-without-exc-info) (`LOG007`)
|
||||
- [`undocumented-warn`](https://docs.astral.sh/ruff/rules/undocumented-warn) (`LOG009`)
|
||||
|
||||
Fixes for the following rules have been stabilized and are now available without preview:
|
||||
|
||||
- [`triple-single-quotes`](https://docs.astral.sh/ruff/rules/triple-single-quotes) (`D300`)
|
||||
- [`non-pep604-annotation`](https://docs.astral.sh/ruff/rules/non-pep604-annotation) (`UP007`)
|
||||
- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default) (`SIM910`)
|
||||
- [`in-dict-keys`](https://docs.astral.sh/ruff/rules/in-dict-keys) (`SIM118`)
|
||||
- [`collapsible-else-if`](https://docs.astral.sh/ruff/rules/collapsible-else-if) (`PLR5501`)
|
||||
- [`if-with-same-arms`](https://docs.astral.sh/ruff/rules/if-with-same-arms) (`SIM114`)
|
||||
- [`useless-else-on-loop`](https://docs.astral.sh/ruff/rules/useless-else-on-loop) (`PLW0120`)
|
||||
- [`unnecessary-literal-union`](https://docs.astral.sh/ruff/rules/unnecessary-literal-union) (`PYI030`)
|
||||
- [`unnecessary-spread`](https://docs.astral.sh/ruff/rules/unnecessary-spread) (`PIE800`)
|
||||
- [`error-instead-of-exception`](https://docs.astral.sh/ruff/rules/error-instead-of-exception) (`TRY400`)
|
||||
- [`redefined-while-unused`](https://docs.astral.sh/ruff/rules/redefined-while-unused) (`F811`)
|
||||
- [`duplicate-value`](https://docs.astral.sh/ruff/rules/duplicate-value) (`B033`)
|
||||
- [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`)
|
||||
- [`non-pep585-annotation`](https://docs.astral.sh/ruff/rules/non-pep585-annotation) (`UP006`)
|
||||
|
||||
Fixes for the following rules have been promoted from unsafe to safe:
|
||||
|
||||
- [`unaliased-collections-abc-set-import`](https://docs.astral.sh/ruff/rules/unaliased-collections-abc-set-import) (`PYI025`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) allows `sys.path` modifications between imports
|
||||
- [`reimplemented-container-builtin`](https://docs.astral.sh/ruff/rules/reimplemented-container-builtin/) (`PIE807`) includes lambdas that can be replaced with `dict`
|
||||
- [`unnecessary-placeholder`](https://docs.astral.sh/ruff/rules/unnecessary-placeholder/) (`PIE790`) applies to unnecessary ellipses (`...`)
|
||||
- [`if-else-block-instead-of-dict-get`](https://docs.astral.sh/ruff/rules/if-else-block-instead-of-dict-get/) (`SIM401`) applies to `if-else` expressions
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurb`\] Implement `metaclass_abcmeta` (`FURB180`) ([#9658](https://github.com/astral-sh/ruff/pull/9658))
|
||||
- Implement `blank_line_after_nested_stub_class` preview style ([#9155](https://github.com/astral-sh/ruff/pull/9155))
|
||||
- The preview rule [`and-or-ternary`](https://docs.astral.sh/ruff/rules/and-or-ternary) (`PLR1706`) was removed
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-async`\] Take `pathlib.Path` into account when analyzing async functions ([#9703](https://github.com/astral-sh/ruff/pull/9703))
|
||||
- \[`flake8-return`\] - fix indentation syntax error (`RET505`) ([#9705](https://github.com/astral-sh/ruff/pull/9705))
|
||||
- Detect multi-statement lines in else removal ([#9748](https://github.com/astral-sh/ruff/pull/9748))
|
||||
- `RUF022`, `RUF023`: never add two trailing commas to the end of a sequence ([#9698](https://github.com/astral-sh/ruff/pull/9698))
|
||||
- `RUF023`: Don't sort `__match_args__`, only `__slots__` ([#9724](https://github.com/astral-sh/ruff/pull/9724))
|
||||
- \[`flake8-simplify`\] - Fix syntax error in autofix (`SIM114`) ([#9704](https://github.com/astral-sh/ruff/pull/9704))
|
||||
- \[`pylint`\] Show verbatim constant in `magic-value-comparison` (`PLR2004`) ([#9694](https://github.com/astral-sh/ruff/pull/9694))
|
||||
- Removing trailing whitespace inside multiline strings is unsafe ([#9744](https://github.com/astral-sh/ruff/pull/9744))
|
||||
- Support `IfExp` with dual string arms in `invalid-envvar-default` ([#9734](https://github.com/astral-sh/ruff/pull/9734))
|
||||
- \[`pylint`\] Add `__mro_entries__` to known dunder methods (`PLW3201`) ([#9706](https://github.com/astral-sh/ruff/pull/9706))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Removed rules are now retained in the documentation ([#9691](https://github.com/astral-sh/ruff/pull/9691))
|
||||
- Deprecated rules are now indicated in the documentation ([#9689](https://github.com/astral-sh/ruff/pull/9689))
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -231,7 +231,7 @@ Once you've completed the code for the rule itself, you can define tests with th
|
||||
For example, if you're adding a new rule named `E402`, you would run:
|
||||
|
||||
```shell
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
|
||||
```
|
||||
|
||||
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
|
||||
|
||||
94
Cargo.lock
generated
94
Cargo.lock
generated
@@ -75,9 +75,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.4"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
|
||||
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -312,9 +312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.13"
|
||||
version = "4.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
|
||||
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -322,9 +322,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.12"
|
||||
version = "4.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9"
|
||||
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1090,9 +1090,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75828adcb53122ef5ea649a39f50f82d94b754099bf6331b32e255e1891e8fb"
|
||||
checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
@@ -1132,9 +1132,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1324,9 +1324,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -1771,21 +1771,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.76"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyproject-toml"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
|
||||
checksum = "ef61ae096a2f8c8b49eca360679dbc25f57c99145f6634b6bc18fedb1f9c6c30"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"pep440_rs 0.3.12",
|
||||
"pep440_rs 0.4.0",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"toml",
|
||||
@@ -2005,7 +2005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.15"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"notify",
|
||||
@@ -2081,7 +2081,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"globset",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"regex",
|
||||
"ruff_macros",
|
||||
"seahash",
|
||||
@@ -2097,7 +2097,7 @@ dependencies = [
|
||||
"imara-diff",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"libcst",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
@@ -2165,7 +2165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.15"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2181,7 +2181,7 @@ dependencies = [
|
||||
"insta",
|
||||
"is-macro",
|
||||
"is-wsl",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"libcst",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2233,7 +2233,7 @@ dependencies = [
|
||||
name = "ruff_macros"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
@@ -2246,7 +2246,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
@@ -2264,10 +2264,12 @@ dependencies = [
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.4.1",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"once_cell",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
@@ -2298,7 +2300,7 @@ dependencies = [
|
||||
"clap",
|
||||
"countme",
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"regex",
|
||||
@@ -2341,7 +2343,7 @@ dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"hexf-parse",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"lexical-parse-float",
|
||||
"rand",
|
||||
"unic-ucd-category",
|
||||
@@ -2355,7 +2357,7 @@ dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"lalrpop",
|
||||
"lalrpop-util",
|
||||
"memchr",
|
||||
@@ -2406,7 +2408,7 @@ name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_index",
|
||||
"ruff_python_parser",
|
||||
@@ -2417,7 +2419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.15"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2488,7 +2490,7 @@ dependencies = [
|
||||
"globset",
|
||||
"ignore",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
@@ -2642,9 +2644,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.195"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2662,9 +2664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.195"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2713,9 +2715,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.5.1"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
|
||||
checksum = "1b0ed1662c5a68664f45b76d18deb0e234aff37207086803165c961eb695e981"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
@@ -2723,9 +2725,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.5.1"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
|
||||
checksum = "568577ff0ef47b879f736cd66740e022f3672788cdf002a05a4e609ea5a6fb15"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -3058,9 +3060,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3079,9 +3081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -3521,9 +3523,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wild"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67"
|
||||
checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1"
|
||||
dependencies = [
|
||||
"glob",
|
||||
]
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -21,7 +21,7 @@ bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.4.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.4.13", features = ["derive"] }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "2.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
|
||||
@@ -47,15 +47,15 @@ indicatif ={ version = "0.17.7"}
|
||||
indoc ={ version = "2.0.4"}
|
||||
insta = { version = "1.34.0", feature = ["filters", "glob"] }
|
||||
insta-cmd = { version = "0.4.0" }
|
||||
is-macro = { version = "0.3.4" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.12.0" }
|
||||
itertools = { version = "0.12.1" }
|
||||
js-sys = { version = "0.3.67" }
|
||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
lexical-parse-float = { version = "0.8.0", features = ["format"] }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = { version = "2.6.4" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version ="0.1.39"}
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
@@ -64,8 +64,8 @@ path-absolutize = { version = "3.1.1" }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.4.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.73" }
|
||||
pyproject-toml = { version = "0.8.1" }
|
||||
proc-macro2 = { version = "1.0.78" }
|
||||
pyproject-toml = { version = "0.8.2" }
|
||||
quick-junit = { version = "0.3.5" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
@@ -76,11 +76,11 @@ rustc-hash = { version = "1.1.0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version ="4.1.0"}
|
||||
semver = { version = "1.0.21" }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde-wasm-bindgen = { version = "0.6.3" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
serde_with = { version = "3.5.1", default-features = false, features = ["macros"] }
|
||||
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
shlex = { version ="1.3.0"}
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
@@ -93,7 +93,7 @@ tempfile = { version ="3.9.0"}
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "1.0.51" }
|
||||
tikv-jemallocator = { version ="0.5.0"}
|
||||
toml = { version = "0.8.8" }
|
||||
toml = { version = "0.8.9" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.15
|
||||
rev: v0.2.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -402,6 +402,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
[Diffusers](https://github.com/huggingface/diffusers))
|
||||
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
|
||||
- [Ibis](https://github.com/ibis-project/ibis)
|
||||
- [ivy](https://github.com/unifyai/ivy)
|
||||
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
|
||||
- [LangChain](https://github.com/hwchase17/langchain)
|
||||
- [Litestar](https://litestar.dev/)
|
||||
@@ -432,6 +433,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
|
||||
- [PyMC](https://github.com/pymc-devs/pymc/)
|
||||
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
|
||||
- [pytest](https://github.com/pytest-dev/pytest)
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
@@ -462,7 +464,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
|
||||
### Show Your Support
|
||||
|
||||
If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
|
||||
If you're using Ruff, consider adding the Ruff badge to your project's `README.md`:
|
||||
|
||||
```md
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
@@ -488,6 +490,6 @@ MIT
|
||||
|
||||
<div align="center">
|
||||
<a target="_blank" href="https://astral.sh" style="background:none">
|
||||
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg">
|
||||
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg" alt="Made by Astral">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.1.15"
|
||||
version = "0.2.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -54,6 +54,8 @@ walkdir = { workspace = true }
|
||||
wild = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Enable test rules during development
|
||||
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
|
||||
assert_cmd = { workspace = true }
|
||||
# Avoid writing colored snapshots when running tests from the terminal
|
||||
colored = { workspace = true, features = ["no-color"]}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::{command, Parser};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -11,7 +15,9 @@ use ruff_linter::settings::types::{
|
||||
ExtensionPair, FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion,
|
||||
SerializationFormat, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::PycodestyleOptions;
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
@@ -104,6 +110,7 @@ pub struct CheckCommand {
|
||||
no_unsafe_fixes: bool,
|
||||
/// Show violations with source code.
|
||||
/// Use `--no-show-source` to disable.
|
||||
/// (Deprecated: use `--output-format=full` or `--output-format=concise` instead of `--show-source` and `--no-show-source`, respectively)
|
||||
#[arg(long, overrides_with("no_show_source"))]
|
||||
show_source: bool,
|
||||
#[clap(long, overrides_with("show_source"), hide = true)]
|
||||
@@ -131,6 +138,8 @@ pub struct CheckCommand {
|
||||
ignore_noqa: bool,
|
||||
|
||||
/// Output serialization format for violations.
|
||||
/// The default serialization format is "concise".
|
||||
/// In preview mode, the default serialization format is "full".
|
||||
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
|
||||
pub output_format: Option<SerializationFormat>,
|
||||
|
||||
@@ -437,6 +446,21 @@ pub struct FormatCommand {
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
|
||||
/// When specified, Ruff will try to only format the code in the given range.
|
||||
/// It might be necessary to extend the start backwards or the end forwards, to fully enclose a logical line.
|
||||
/// The `<RANGE>` uses the format `<start_line>:<start_column>-<end_line>:<end_column>`.
|
||||
///
|
||||
/// - The line and column numbers are 1 based.
|
||||
/// - The column specifies the nth-unicode codepoint on that line.
|
||||
/// - The end offset is exclusive.
|
||||
/// - The column numbers are optional. You can write `--range=1-2` instead of `--range=1:1-2:1`.
|
||||
/// - The end position is optional. You can write `--range=2` to format the entire document starting from the second line.
|
||||
/// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
|
||||
///
|
||||
/// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
|
||||
#[clap(long, help_heading = "Editor options", verbatim_doc_comment)]
|
||||
pub range: Option<FormatRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
@@ -533,7 +557,6 @@ impl CheckCommand {
|
||||
self.no_respect_gitignore,
|
||||
),
|
||||
select: self.select,
|
||||
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
|
||||
target_version: self.target_version,
|
||||
unfixable: self.unfixable,
|
||||
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
|
||||
@@ -543,7 +566,11 @@ impl CheckCommand {
|
||||
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
|
||||
.map(UnsafeFixes::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
output_format: self.output_format,
|
||||
output_format: resolve_output_format(
|
||||
self.output_format,
|
||||
resolve_bool_arg(self.show_source, self.no_show_source),
|
||||
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
|
||||
),
|
||||
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
|
||||
extension: self.extension,
|
||||
},
|
||||
@@ -564,6 +591,7 @@ impl FormatCommand {
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
range: self.range,
|
||||
},
|
||||
CliOverrides {
|
||||
line_length: self.line_length,
|
||||
@@ -594,6 +622,43 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_output_format(
|
||||
output_format: Option<SerializationFormat>,
|
||||
show_sources: Option<bool>,
|
||||
preview: bool,
|
||||
) -> Option<SerializationFormat> {
|
||||
Some(match (output_format, show_sources) {
|
||||
(Some(o), None) => o,
|
||||
(Some(SerializationFormat::Grouped), Some(true)) => {
|
||||
warn_user!("`--show-source` with `--output-format=grouped` is deprecated, and will not show source files. Use `--output-format=full` to show source information.");
|
||||
SerializationFormat::Grouped
|
||||
}
|
||||
(Some(fmt), Some(true)) => {
|
||||
warn_user!("The `--show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
|
||||
fmt
|
||||
}
|
||||
(Some(fmt), Some(false)) => {
|
||||
warn_user!("The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
|
||||
fmt
|
||||
}
|
||||
(None, Some(true)) => {
|
||||
warn_user!("The `--show-source` argument is deprecated. Use `--output-format=full` instead.");
|
||||
SerializationFormat::Full
|
||||
}
|
||||
(None, Some(false)) => {
|
||||
warn_user!("The `--no-show-source` argument is deprecated. Use `--output-format=concise` instead.");
|
||||
SerializationFormat::Concise
|
||||
}
|
||||
(None, None) => return None
|
||||
}).map(|format| match format {
|
||||
SerializationFormat::Text => {
|
||||
warn_user!("`--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `{}`.", SerializationFormat::default(preview));
|
||||
SerializationFormat::default(preview)
|
||||
},
|
||||
other => other
|
||||
})
|
||||
}
|
||||
|
||||
/// CLI settings that are distinct from configuration (commands, lists of files,
|
||||
/// etc.).
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
@@ -627,6 +692,196 @@ pub struct FormatArguments {
|
||||
pub files: Vec<PathBuf>,
|
||||
pub isolated: bool,
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
pub range: Option<FormatRange>,
|
||||
}
|
||||
|
||||
/// A text range specified by line and column numbers.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct FormatRange {
|
||||
start: LineColumn,
|
||||
end: LineColumn,
|
||||
}
|
||||
|
||||
impl FormatRange {
|
||||
/// Converts the line:column range to a byte offset range specific for `source`.
|
||||
///
|
||||
/// Returns an empty range if the start range is past the end of `source`.
|
||||
pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange {
|
||||
let start_byte_offset = line_index.offset(self.start.line, self.start.column, source);
|
||||
let end_byte_offset = line_index.offset(self.end.line, self.end.column, source);
|
||||
|
||||
TextRange::new(start_byte_offset, end_byte_offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FormatRange {
|
||||
type Err = FormatRangeParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let (start, end) = value.split_once('-').unwrap_or((value, ""));
|
||||
|
||||
let start = if start.is_empty() {
|
||||
LineColumn::default()
|
||||
} else {
|
||||
start.parse().map_err(FormatRangeParseError::InvalidStart)?
|
||||
};
|
||||
|
||||
let end = if end.is_empty() {
|
||||
LineColumn {
|
||||
line: OneIndexed::MAX,
|
||||
column: OneIndexed::MAX,
|
||||
}
|
||||
} else {
|
||||
end.parse().map_err(FormatRangeParseError::InvalidEnd)?
|
||||
};
|
||||
|
||||
if start > end {
|
||||
return Err(FormatRangeParseError::StartGreaterThanEnd(start, end));
|
||||
}
|
||||
|
||||
Ok(FormatRange { start, end })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FormatRangeParseError {
|
||||
InvalidStart(LineColumnParseError),
|
||||
InvalidEnd(LineColumnParseError),
|
||||
|
||||
StartGreaterThanEnd(LineColumn, LineColumn),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FormatRangeParseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let tip = " tip:".bold().green();
|
||||
match self {
|
||||
FormatRangeParseError::StartGreaterThanEnd(start, end) => {
|
||||
write!(
|
||||
f,
|
||||
"the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'",
|
||||
start_invalid=start.to_string().bold().yellow(),
|
||||
end_invalid=end.to_string().bold().yellow(),
|
||||
start=start.to_string().green().bold(),
|
||||
end=end.to_string().green().bold()
|
||||
)
|
||||
}
|
||||
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
|
||||
FormatRangeParseError::InvalidEnd(inner) => inner.write(f, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FormatRangeParseError {}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct LineColumn {
|
||||
pub line: OneIndexed,
|
||||
pub column: OneIndexed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LineColumn {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{line}:{column}", line = self.line, column = self.column)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LineColumn {
|
||||
fn default() -> Self {
|
||||
LineColumn {
|
||||
line: OneIndexed::MIN,
|
||||
column: OneIndexed::MIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for LineColumn {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for LineColumn {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.line
|
||||
.cmp(&other.line)
|
||||
.then(self.column.cmp(&other.column))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LineColumn {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LineColumn {}
|
||||
|
||||
impl FromStr for LineColumn {
|
||||
type Err = LineColumnParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let (line, column) = value.split_once(':').unwrap_or((value, "1"));
|
||||
|
||||
let line: usize = line.parse().map_err(LineColumnParseError::LineParseError)?;
|
||||
let column: usize = column
|
||||
.parse()
|
||||
.map_err(LineColumnParseError::ColumnParseError)?;
|
||||
|
||||
match (OneIndexed::new(line), OneIndexed::new(column)) {
|
||||
(Some(line), Some(column)) => Ok(LineColumn { line, column }),
|
||||
(Some(line), None) => Err(LineColumnParseError::ZeroColumnIndex { line }),
|
||||
(None, Some(column)) => Err(LineColumnParseError::ZeroLineIndex { column }),
|
||||
(None, None) => Err(LineColumnParseError::ZeroLineAndColumnIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LineColumnParseError {
|
||||
ZeroLineIndex { column: OneIndexed },
|
||||
ZeroColumnIndex { line: OneIndexed },
|
||||
ZeroLineAndColumnIndex,
|
||||
LineParseError(std::num::ParseIntError),
|
||||
ColumnParseError(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl LineColumnParseError {
|
||||
fn write(&self, f: &mut std::fmt::Formatter, start_range: bool) -> std::fmt::Result {
|
||||
let tip = "tip:".bold().green();
|
||||
|
||||
let range = if start_range { "start" } else { "end" };
|
||||
|
||||
match self {
|
||||
LineColumnParseError::ColumnParseError(inner) => {
|
||||
write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.")
|
||||
}
|
||||
LineColumnParseError::LineParseError(inner) => {
|
||||
write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.")
|
||||
}
|
||||
LineColumnParseError::ZeroColumnIndex { line } => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion=format!("{line}:1").green().bold()
|
||||
)
|
||||
}
|
||||
LineColumnParseError::ZeroLineIndex { column } => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion=format!("1:{column}").green().bold()
|
||||
)
|
||||
}
|
||||
LineColumnParseError::ZeroLineAndColumnIndex => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion="1:1".to_string().green().bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI settings that function as configuration overrides.
|
||||
@@ -648,7 +903,6 @@ pub struct CliOverrides {
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub select: Option<Vec<RuleSelector>>,
|
||||
pub show_source: Option<bool>,
|
||||
pub target_version: Option<PythonVersion>,
|
||||
pub unfixable: Option<Vec<RuleSelector>>,
|
||||
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
|
||||
@@ -735,9 +989,6 @@ impl ConfigurationTransformer for CliOverrides {
|
||||
if let Some(respect_gitignore) = &self.respect_gitignore {
|
||||
config.respect_gitignore = Some(*respect_gitignore);
|
||||
}
|
||||
if let Some(show_source) = &self.show_source {
|
||||
config.show_source = Some(*show_source);
|
||||
}
|
||||
if let Some(show_fixes) = &self.show_fixes {
|
||||
config.show_fixes = Some(*show_fixes);
|
||||
}
|
||||
|
||||
@@ -1050,6 +1050,7 @@ mod tests {
|
||||
&self.settings.formatter,
|
||||
PySourceType::Python,
|
||||
FormatMode::Write,
|
||||
None,
|
||||
Some(cache),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,12 +23,13 @@ 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, QuoteStyle};
|
||||
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
@@ -77,6 +78,13 @@ pub(crate) fn format(
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if cli.range.is_some() && paths.len() > 1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
||||
paths.len()
|
||||
));
|
||||
}
|
||||
|
||||
warn_incompatible_formatter_settings(&resolver);
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
@@ -139,7 +147,14 @@ pub(crate) fn format(
|
||||
|
||||
Some(
|
||||
match catch_unwind(|| {
|
||||
format_path(path, &settings.formatter, source_type, mode, cache)
|
||||
format_path(
|
||||
path,
|
||||
&settings.formatter,
|
||||
source_type,
|
||||
mode,
|
||||
cli.range,
|
||||
cache,
|
||||
)
|
||||
}) {
|
||||
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||
path: resolved_file.path().to_path_buf(),
|
||||
@@ -226,6 +241,7 @@ pub(crate) fn format_path(
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
range: Option<FormatRange>,
|
||||
cache: Option<&Cache>,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
if let Some(cache) = cache {
|
||||
@@ -250,8 +266,12 @@ pub(crate) fn format_path(
|
||||
}
|
||||
};
|
||||
|
||||
// Don't write back to the cache if formatting a range.
|
||||
let cache = cache.filter(|_| range.is_none());
|
||||
|
||||
// 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, range)?
|
||||
{
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::Write => {
|
||||
let mut writer = File::create(path).map_err(|err| {
|
||||
@@ -319,12 +339,31 @@ pub(crate) fn format_source(
|
||||
source_type: PySourceType,
|
||||
path: Option<&Path>,
|
||||
settings: &FormatterSettings,
|
||||
range: Option<FormatRange>,
|
||||
) -> Result<FormattedSource, FormatCommandError> {
|
||||
match &source_kind {
|
||||
SourceKind::Python(unformatted) => {
|
||||
let options = settings.to_format_options(source_type, unformatted);
|
||||
|
||||
let formatted = format_module_source(unformatted, options).map_err(|err| {
|
||||
let formatted = if let Some(range) = range {
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||
let mut formatted = unformatted.to_string();
|
||||
formatted.replace_range(
|
||||
std::ops::Range::<usize>::from(formatted_range.source_range()),
|
||||
formatted_range.as_code(),
|
||||
);
|
||||
|
||||
formatted
|
||||
})
|
||||
} else {
|
||||
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
format_module_source(unformatted, options).map(|formatted| formatted.into_code())
|
||||
};
|
||||
|
||||
let formatted = formatted.map_err(|err| {
|
||||
if let FormatModuleError::ParseError(err) = err {
|
||||
DisplayParseError::from_source_kind(
|
||||
err,
|
||||
@@ -337,7 +376,6 @@ pub(crate) fn format_source(
|
||||
}
|
||||
})?;
|
||||
|
||||
let formatted = formatted.into_code();
|
||||
if formatted.len() == unformatted.len() && formatted == *unformatted {
|
||||
Ok(FormattedSource::Unchanged)
|
||||
} else {
|
||||
@@ -349,6 +387,12 @@ pub(crate) fn format_source(
|
||||
return Ok(FormattedSource::Unchanged);
|
||||
}
|
||||
|
||||
if range.is_some() {
|
||||
return Err(FormatCommandError::RangeFormatNotebook(
|
||||
path.map(Path::to_path_buf),
|
||||
));
|
||||
}
|
||||
|
||||
let options = settings.to_format_options(source_type, notebook.source_code());
|
||||
|
||||
let mut output: Option<String> = None;
|
||||
@@ -589,6 +633,7 @@ pub(crate) enum FormatCommandError {
|
||||
Format(Option<PathBuf>, FormatModuleError),
|
||||
Write(Option<PathBuf>, SourceError),
|
||||
Diff(Option<PathBuf>, io::Error),
|
||||
RangeFormatNotebook(Option<PathBuf>),
|
||||
}
|
||||
|
||||
impl FormatCommandError {
|
||||
@@ -606,7 +651,8 @@ impl FormatCommandError {
|
||||
| Self::Read(path, _)
|
||||
| Self::Format(path, _)
|
||||
| Self::Write(path, _)
|
||||
| Self::Diff(path, _) => path.as_deref(),
|
||||
| Self::Diff(path, _)
|
||||
| Self::RangeFormatNotebook(path) => path.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -628,9 +674,10 @@ impl Display for FormatCommandError {
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
"Encountered error:".bold(),
|
||||
err.io_error()
|
||||
"{header} {error}",
|
||||
header = "Encountered error:".bold(),
|
||||
error = err
|
||||
.io_error()
|
||||
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
|
||||
)
|
||||
}
|
||||
@@ -648,7 +695,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to read:".bold())
|
||||
}
|
||||
}
|
||||
Self::Write(path, err) => {
|
||||
@@ -661,7 +708,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to write:".bold())
|
||||
}
|
||||
}
|
||||
Self::Format(path, err) => {
|
||||
@@ -674,7 +721,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to format:".bold())
|
||||
}
|
||||
}
|
||||
Self::Diff(path, err) => {
|
||||
@@ -689,9 +736,25 @@ impl Display for FormatCommandError {
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}{} {err}",
|
||||
"Failed to generate diff".bold(),
|
||||
":".bold()
|
||||
"{header} {err}",
|
||||
header = "Failed to generate diff:".bold(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::RangeFormatNotebook(path) => {
|
||||
if let Some(path) = path {
|
||||
write!(
|
||||
f,
|
||||
"{header}{path}{colon} Range formatting isn't supported for notebooks.",
|
||||
header = "Failed to format ".bold(),
|
||||
path = fs::relativize_path(path).bold(),
|
||||
colon = ":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{header} Range formatting isn't supported for notebooks",
|
||||
header = "Failed to format:".bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::commands::format::{
|
||||
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
||||
FormatResult, FormattedSource,
|
||||
@@ -69,7 +69,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
};
|
||||
|
||||
// Format the file.
|
||||
match format_source_code(path, settings, source_type, mode) {
|
||||
match format_source_code(path, cli.range, settings, source_type, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
@@ -90,6 +90,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
/// Format source code read from `stdin`.
|
||||
fn format_source_code(
|
||||
path: Option<&Path>,
|
||||
range: Option<FormatRange>,
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
@@ -107,7 +108,7 @@ fn format_source_code(
|
||||
};
|
||||
|
||||
// Format the source.
|
||||
let formatted = format_source(&source_kind, source_type, path, settings)?;
|
||||
let formatted = format_source(&source_kind, source_type, path, settings, range)?;
|
||||
|
||||
match &formatted {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
|
||||
@@ -255,7 +255,6 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
unsafe_fixes,
|
||||
output_format,
|
||||
show_fixes,
|
||||
show_source,
|
||||
..
|
||||
} = pyproject_config.settings;
|
||||
|
||||
@@ -284,9 +283,6 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
if show_fixes {
|
||||
printer_flags |= PrinterFlags::SHOW_FIX_SUMMARY;
|
||||
}
|
||||
if show_source {
|
||||
printer_flags |= PrinterFlags::SHOW_SOURCE;
|
||||
}
|
||||
if cli.ecosystem_ci {
|
||||
warn_user!(
|
||||
"The formatting of fixes emitted by this option is a work-in-progress, subject to \
|
||||
@@ -325,9 +321,18 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
printer_flags,
|
||||
);
|
||||
|
||||
// the settings should already be combined with the CLI overrides at this point
|
||||
// TODO(jane): let's make this `PreviewMode`
|
||||
// TODO: this should reference the global preview mode once https://github.com/astral-sh/ruff/issues/8232
|
||||
// is resolved.
|
||||
let preview = pyproject_config.settings.linter.preview.is_enabled();
|
||||
|
||||
if cli.watch {
|
||||
if output_format != SerializationFormat::Text {
|
||||
warn_user!("`--output-format text` is always used in watch mode.");
|
||||
if output_format != SerializationFormat::default(preview) {
|
||||
warn_user!(
|
||||
"`--output-format {}` is always used in watch mode.",
|
||||
SerializationFormat::default(preview)
|
||||
);
|
||||
}
|
||||
|
||||
// Configure the file watcher.
|
||||
@@ -353,7 +358,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
)?;
|
||||
printer.write_continuously(&mut writer, &messages)?;
|
||||
printer.write_continuously(&mut writer, &messages, preview)?;
|
||||
|
||||
// In watch mode, we may need to re-resolve the configuration.
|
||||
// TODO(charlie): Re-compute other derivative values, like the `printer`.
|
||||
@@ -386,7 +391,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
)?;
|
||||
printer.write_continuously(&mut writer, &messages)?;
|
||||
printer.write_continuously(&mut writer, &messages, preview)?;
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ bitflags! {
|
||||
pub(crate) struct Flags: u8 {
|
||||
/// Whether to show violations when emitting diagnostics.
|
||||
const SHOW_VIOLATIONS = 0b0000_0001;
|
||||
/// Whether to show the source code when emitting diagnostics.
|
||||
const SHOW_SOURCE = 0b000_0010;
|
||||
/// Whether to show a summary of the fixed violations when emitting diagnostics.
|
||||
const SHOW_FIX_SUMMARY = 0b0000_0100;
|
||||
/// Whether to show a diff of each fixed violation when emitting diagnostics.
|
||||
@@ -218,7 +216,10 @@ impl Printer {
|
||||
if !self.flags.intersects(Flags::SHOW_VIOLATIONS) {
|
||||
if matches!(
|
||||
self.format,
|
||||
SerializationFormat::Text | SerializationFormat::Grouped
|
||||
SerializationFormat::Text
|
||||
| SerializationFormat::Full
|
||||
| SerializationFormat::Concise
|
||||
| SerializationFormat::Grouped
|
||||
) {
|
||||
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
@@ -245,11 +246,12 @@ impl Printer {
|
||||
SerializationFormat::Junit => {
|
||||
JunitEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Text => {
|
||||
SerializationFormat::Concise
|
||||
| SerializationFormat::Full => {
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
|
||||
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
|
||||
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
|
||||
.with_show_source(self.format == SerializationFormat::Full)
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.emit(writer, &diagnostics.messages, &context)?;
|
||||
|
||||
@@ -265,7 +267,6 @@ impl Printer {
|
||||
}
|
||||
SerializationFormat::Grouped => {
|
||||
GroupedEmitter::default()
|
||||
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
|
||||
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.emit(writer, &diagnostics.messages, &context)?;
|
||||
@@ -294,6 +295,7 @@ impl Printer {
|
||||
SerializationFormat::Sarif => {
|
||||
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
@@ -342,7 +344,9 @@ impl Printer {
|
||||
}
|
||||
|
||||
match self.format {
|
||||
SerializationFormat::Text => {
|
||||
SerializationFormat::Text
|
||||
| SerializationFormat::Full
|
||||
| SerializationFormat::Concise => {
|
||||
// Compute the maximum number of digits in the count and code, for all messages,
|
||||
// to enable pretty-printing.
|
||||
let count_width = num_digits(
|
||||
@@ -403,6 +407,7 @@ impl Printer {
|
||||
&self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &Diagnostics,
|
||||
preview: bool,
|
||||
) -> Result<()> {
|
||||
if matches!(self.log_level, LogLevel::Silent) {
|
||||
return Ok(());
|
||||
@@ -430,7 +435,7 @@ impl Printer {
|
||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
|
||||
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
|
||||
.with_show_source(preview)
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
|
||||
149
crates/ruff/tests/deprecation.rs
Normal file
149
crates/ruff/tests/deprecation.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
//! A test suite that ensures deprecated command line options have appropriate warnings / behaviors
|
||||
|
||||
use ruff_linter::settings::types::SerializationFormat;
|
||||
use std::process::Command;
|
||||
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
|
||||
const BIN_NAME: &str = "ruff";
|
||||
|
||||
const STDIN: &str = "l = 1";
|
||||
|
||||
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
|
||||
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
|
||||
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
|
||||
cmd.arg("--output-format");
|
||||
cmd.arg(output_format);
|
||||
cmd.arg("--no-cache");
|
||||
match show_source {
|
||||
Some(true) => {
|
||||
cmd.arg("--show-source");
|
||||
}
|
||||
Some(false) => {
|
||||
cmd.arg("--no-show-source");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
cmd.arg("-");
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_show_source_is_deprecated() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(true), None).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_no_show_source_is_deprecated() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(false), None).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_output_format_is_deprecated() {
|
||||
assert_cmd_snapshot!(ruff_check(None, Some("text".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_output_format_overrides_show_source() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(true), Some("concise".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_full_output_format_overrides_no_show_source() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(false), Some("full".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
|
|
||||
1 | l = 1
|
||||
| ^ E741
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=full`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_output_format_uses_concise_over_no_show_source() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(false), Some("concise".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_deprecated_output_format_overrides_show_source() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(true), Some("text".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
|
||||
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_deprecated_output_format_overrides_no_show_source() {
|
||||
assert_cmd_snapshot!(ruff_check(Some(false), Some("text".into())).pass_stdin(STDIN), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
|
||||
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
|
||||
"###);
|
||||
}
|
||||
@@ -508,6 +508,9 @@ if __name__ == '__main__':
|
||||
say_hy("dear Ruff contributor")
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
- 'ignore' -> 'lint.ignore'
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -546,6 +549,9 @@ if __name__ == '__main__':
|
||||
say_hy("dear Ruff contributor")
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
- 'ignore' -> 'lint.ignore'
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1538,3 +1544,322 @@ include = ["*.ipy"]
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:8-2:14"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_unicode() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:21-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1="👋🏽" ): print("Format this" )
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1="👋🏽" ):
|
||||
print("Format this")
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_multiple_files() -> std::io::Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let file1 = tempdir.path().join("file1.py");
|
||||
|
||||
fs::write(
|
||||
&file1,
|
||||
r#"
|
||||
def file1(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let file2 = tempdir.path().join("file2.py");
|
||||
|
||||
fs::write(
|
||||
&file2,
|
||||
r#"
|
||||
def file2(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--range=1:8-1:15"])
|
||||
.arg(file1)
|
||||
.arg(file2), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: The `--range` option is only supported when formatting a single file but the specified paths resolve to 2 files.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_out_of_bounds() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=100:40-200:1"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_start_larger_than_end() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=90-50"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '90-50' for '--range <RANGE>': the start position '90:1' is greater than the end position '50:1'.
|
||||
tip: Try switching start and end: '50:1-90:1'
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_line_numbers_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_start_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this")
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_end_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Should format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_missing_line() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=1-:20"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '1-:20' for '--range <RANGE>': the end line is not a valid number (cannot parse integer from empty string)
|
||||
tip: The format is 'line:column'.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_line_number() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:2"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '0:2' for '--range <RANGE>': the start line is 0, but it should be 1 or greater.
|
||||
tip: The line numbers start at 1.
|
||||
tip: Try 1:2 instead.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_and_line_zero() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:0"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '0:0' for '--range <RANGE>': the start line and column are both 0, but they should be 1 or greater.
|
||||
tip: The line and column numbers start at 1.
|
||||
tip: Try 1:1 instead.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_notebook() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--no-cache", "--stdin-filename", "main.ipynb", "--range=1-2"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x=1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
|
||||
"###);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
#![cfg(not(target_family = "wasm"))]
|
||||
|
||||
use regex::escape;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
@@ -11,7 +12,11 @@ use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use tempfile::TempDir;
|
||||
|
||||
const BIN_NAME: &str = "ruff";
|
||||
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "text"];
|
||||
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "concise"];
|
||||
|
||||
fn tempdir_filter(tempdir: &TempDir) -> String {
|
||||
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_options() -> Result<()> {
|
||||
@@ -27,24 +32,32 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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#"a = "abcba".strip("aba")"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
|
||||
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
||||
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
|
||||
Found 3 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
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#"a = "abcba".strip("aba")"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
|
||||
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
||||
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
|
||||
Found 3 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
- 'flake8-quotes' -> 'lint.flake8-quotes'
|
||||
"###);
|
||||
});
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -63,6 +76,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
@@ -80,6 +96,8 @@ inline-quotes = "single"
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,6 +116,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
@@ -114,7 +135,11 @@ inline-quotes = "single"
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,6 +162,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
@@ -153,7 +181,11 @@ inline-quotes = "single"
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
|
||||
- 'flake8-quotes' -> 'lint.flake8-quotes'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -209,6 +241,9 @@ OTHER = "OTHER"
|
||||
|
||||
fs::write(out_dir.join("a.py"), r#"a = "a""#)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
@@ -228,7 +263,11 @@ OTHER = "OTHER"
|
||||
[*] 3 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -249,6 +288,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
@@ -271,7 +313,11 @@ if __name__ == "__main__":
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -290,6 +336,9 @@ max-line-length = 100
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
@@ -309,7 +358,12 @@ _ = "---------------------------------------------------------------------------
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`:
|
||||
- 'select' -> 'lint.select'
|
||||
- 'pycodestyle' -> 'lint.pycodestyle'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -327,6 +381,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
@@ -351,7 +408,11 @@ if __name__ == "__main__":
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -369,6 +430,9 @@ inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
@@ -393,7 +457,11 @@ if __name__ == "__main__":
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`:
|
||||
- 'extend-select' -> 'lint.extend-select'
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -422,6 +490,9 @@ ignore = ["D203", "D212"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(sub_dir)
|
||||
.arg("check")
|
||||
@@ -434,6 +505,8 @@ ignore = ["D203", "D212"]
|
||||
----- stderr -----
|
||||
warning: No Python files found under the given path(s)
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -490,6 +563,9 @@ include = ["*.ipy"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
@@ -506,5 +582,7 @@ include = ["*.ipy"]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ fn check_project_include_defaults() {
|
||||
[BASEPATH]/include-test/subdirectory/c.py
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `nested-project/pyproject.toml`:
|
||||
- 'select' -> 'lint.select'
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ else:
|
||||
```
|
||||
|
||||
## Options
|
||||
- `pyflakes.extend-generics`
|
||||
- `lint.pyflakes.extend-generics`
|
||||
|
||||
## References
|
||||
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
|
||||
|
||||
@@ -17,9 +17,8 @@ Settings path: "[BASEPATH]/pyproject.toml"
|
||||
cache_dir = "[BASEPATH]/.ruff_cache"
|
||||
fix = false
|
||||
fix_only = false
|
||||
output_format = text
|
||||
output_format = concise
|
||||
show_fixes = false
|
||||
show_source = false
|
||||
unsafe_fixes = hint
|
||||
|
||||
# File Resolver Settings
|
||||
|
||||
@@ -26,9 +26,9 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
for rule in Rule::iter() {
|
||||
if let Some(explanation) = rule.explanation() {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code()));
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
|
||||
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
|
||||
if linter.url().is_some() {
|
||||
@@ -37,6 +37,22 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_deprecated() {
|
||||
output.push_str(
|
||||
r"**Warning: This rule is deprecated and will be removed in a future release.**",
|
||||
);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_removed() {
|
||||
output.push_str(
|
||||
r"**Warning: This rule has been removed and its documentation is only available for historical reasons.**",
|
||||
);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let fix_availability = rule.fixable();
|
||||
if matches!(
|
||||
fix_availability,
|
||||
@@ -116,7 +132,7 @@ fn process_documentation(documentation: &str, out: &mut String, rule_name: &str)
|
||||
}
|
||||
}
|
||||
|
||||
let anchor = option.replace('.', "-");
|
||||
let anchor = option.replace('.', "_");
|
||||
out.push_str(&format!("- [`{option}`][{option}]\n"));
|
||||
after.push_str(&format!("[{option}]: ../settings.md#{anchor}\n"));
|
||||
|
||||
@@ -142,13 +158,13 @@ mod tests {
|
||||
let mut output = String::new();
|
||||
process_documentation(
|
||||
"
|
||||
See also [`mccabe.max-complexity`] and [`task-tags`].
|
||||
See also [`lint.mccabe.max-complexity`] and [`lint.task-tags`].
|
||||
Something [`else`][other].
|
||||
|
||||
## Options
|
||||
|
||||
- `task-tags`
|
||||
- `mccabe.max-complexity`
|
||||
- `lint.task-tags`
|
||||
- `lint.mccabe.max-complexity`
|
||||
|
||||
[other]: http://example.com.",
|
||||
&mut output,
|
||||
@@ -157,18 +173,18 @@ Something [`else`][other].
|
||||
assert_eq!(
|
||||
output,
|
||||
"
|
||||
See also [`mccabe.max-complexity`][mccabe.max-complexity] and [`task-tags`][task-tags].
|
||||
See also [`lint.mccabe.max-complexity`][lint.mccabe.max-complexity] and [`lint.task-tags`][lint.task-tags].
|
||||
Something [`else`][other].
|
||||
|
||||
## Options
|
||||
|
||||
- [`task-tags`][task-tags]
|
||||
- [`mccabe.max-complexity`][mccabe.max-complexity]
|
||||
- [`lint.task-tags`][lint.task-tags]
|
||||
- [`lint.mccabe.max-complexity`][lint.mccabe.max-complexity]
|
||||
|
||||
[other]: http://example.com.
|
||||
|
||||
[task-tags]: ../settings.md#task-tags
|
||||
[mccabe.max-complexity]: ../settings.md#mccabe-max-complexity
|
||||
[lint.task-tags]: ../settings.md#lint_task-tags
|
||||
[lint.mccabe.max-complexity]: ../settings.md#lint_mccabe_max-complexity
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
|
||||
//!
|
||||
//! Used for <https://docs.astral.sh/ruff/settings/>.
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_python_trivia::textwrap;
|
||||
@@ -9,16 +10,29 @@ use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visi
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
generate_set(&mut output, &Set::Toplevel(Options::metadata()));
|
||||
|
||||
generate_set(
|
||||
&mut output,
|
||||
Set::Toplevel(Options::metadata()),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn generate_set(output: &mut String, set: &Set) {
|
||||
if set.level() < 2 {
|
||||
writeln!(output, "### {title}\n", title = set.title()).unwrap();
|
||||
} else {
|
||||
writeln!(output, "#### {title}\n", title = set.title()).unwrap();
|
||||
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
match &set {
|
||||
Set::Toplevel(_) => {
|
||||
output.push_str("### Top-level\n");
|
||||
}
|
||||
Set::Named { name, .. } => {
|
||||
let title = parents
|
||||
.iter()
|
||||
.filter_map(|set| set.name())
|
||||
.chain(std::iter::once(name.as_str()))
|
||||
.join(".");
|
||||
writeln!(output, "#### `{title}`\n",).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(documentation) = set.metadata().documentation() {
|
||||
@@ -35,72 +49,68 @@ fn generate_set(output: &mut String, set: &Set) {
|
||||
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
|
||||
parents.push(set);
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, set);
|
||||
emit_field(output, name, field, parents.as_slice());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-sets.
|
||||
for (set_name, sub_set) in &sets {
|
||||
generate_set(output, &Set::Named(set_name, *sub_set, set.level() + 1));
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
enum Set<'a> {
|
||||
enum Set {
|
||||
Toplevel(OptionSet),
|
||||
Named(&'a str, OptionSet, u32),
|
||||
Named { name: String, set: OptionSet },
|
||||
}
|
||||
|
||||
impl<'a> Set<'a> {
|
||||
fn name(&self) -> Option<&'a str> {
|
||||
impl Set {
|
||||
fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Set::Toplevel(_) => None,
|
||||
Set::Named(name, _, _) => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> &'a str {
|
||||
match self {
|
||||
Set::Toplevel(_) => "Top-level",
|
||||
Set::Named(name, _, _) => name,
|
||||
Set::Named { name, .. } => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &OptionSet {
|
||||
match self {
|
||||
Set::Toplevel(set) => set,
|
||||
Set::Named(_, set, _) => set,
|
||||
}
|
||||
}
|
||||
|
||||
fn level(&self) -> u32 {
|
||||
match self {
|
||||
Set::Toplevel(_) => 0,
|
||||
Set::Named(_, _, level) => *level,
|
||||
Set::Named { set, .. } => set,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set: &Set) {
|
||||
let header_level = if parent_set.level() < 2 {
|
||||
"####"
|
||||
} else {
|
||||
"#####"
|
||||
};
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
|
||||
let header_level = if parents.is_empty() { "####" } else { "#####" };
|
||||
let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_");
|
||||
|
||||
if parents_anchor.is_empty() {
|
||||
output.push_str(&format!(
|
||||
"{header_level} [`{name}`](#{name}) {{: #{name} }}\n"
|
||||
));
|
||||
} else {
|
||||
output.push_str(&format!(
|
||||
"{header_level} [`{name}`](#{parents_anchor}_{name}) {{: #{parents_anchor}_{name} }}\n"
|
||||
));
|
||||
|
||||
// if there's a set name, we need to add it to the anchor
|
||||
if let Some(set_name) = parent_set.name() {
|
||||
// the anchor used to just be the name, but now it's the group name
|
||||
// for backwards compatibility, we need to keep the old anchor
|
||||
output.push_str(&format!("<span id=\"{name}\"></span>\n"));
|
||||
|
||||
output.push_str(&format!(
|
||||
"{header_level} [`{name}`](#{set_name}-{name}) {{: #{set_name}-{name} }}\n"
|
||||
));
|
||||
} else {
|
||||
output.push_str(&format!("{header_level} [`{name}`](#{name})\n"));
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
|
||||
if let Some(deprecated) = &field.deprecated {
|
||||
@@ -129,12 +139,12 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
|
||||
output.push_str("**Example usage**:\n\n");
|
||||
output.push_str(&format_tab(
|
||||
"pyproject.toml",
|
||||
&format_header(field.scope, parent_set, ConfigurationFile::PyprojectToml),
|
||||
&format_header(field.scope, parents, ConfigurationFile::PyprojectToml),
|
||||
field.example,
|
||||
));
|
||||
output.push_str(&format_tab(
|
||||
"ruff.toml",
|
||||
&format_header(field.scope, parent_set, ConfigurationFile::RuffToml),
|
||||
&format_header(field.scope, parents, ConfigurationFile::RuffToml),
|
||||
field.example,
|
||||
));
|
||||
output.push('\n');
|
||||
@@ -152,52 +162,22 @@ fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
|
||||
/// Format the TOML header for the example usage for a given option.
|
||||
///
|
||||
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
|
||||
fn format_header(
|
||||
scope: Option<&str>,
|
||||
parent_set: &Set,
|
||||
configuration: ConfigurationFile,
|
||||
) -> String {
|
||||
match configuration {
|
||||
ConfigurationFile::PyprojectToml => {
|
||||
let mut header = if let Some(set_name) = parent_set.name() {
|
||||
if set_name == "format" {
|
||||
String::from("tool.ruff.format")
|
||||
} else {
|
||||
format!("tool.ruff.lint.{set_name}")
|
||||
}
|
||||
} else {
|
||||
"tool.ruff".to_string()
|
||||
};
|
||||
if let Some(scope) = scope {
|
||||
if !header.is_empty() {
|
||||
header.push('.');
|
||||
}
|
||||
header.push_str(scope);
|
||||
}
|
||||
format!("[{header}]")
|
||||
}
|
||||
ConfigurationFile::RuffToml => {
|
||||
let mut header = if let Some(set_name) = parent_set.name() {
|
||||
if set_name == "format" {
|
||||
String::from("format")
|
||||
} else {
|
||||
format!("lint.{set_name}")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if let Some(scope) = scope {
|
||||
if !header.is_empty() {
|
||||
header.push('.');
|
||||
}
|
||||
header.push_str(scope);
|
||||
}
|
||||
if header.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{header}]")
|
||||
}
|
||||
}
|
||||
fn format_header(scope: Option<&str>, parents: &[Set], configuration: ConfigurationFile) -> String {
|
||||
let tool_parent = match configuration {
|
||||
ConfigurationFile::PyprojectToml => Some("tool.ruff"),
|
||||
ConfigurationFile::RuffToml => None,
|
||||
};
|
||||
|
||||
let header = tool_parent
|
||||
.into_iter()
|
||||
.chain(parents.iter().filter_map(|parent| parent.name()))
|
||||
.chain(scope)
|
||||
.join(".");
|
||||
|
||||
if header.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{header}]")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Used for <https://docs.astral.sh/ruff/rules/>.
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_linter::codes::RuleGroup;
|
||||
use std::borrow::Cow;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -14,6 +15,10 @@ use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
const REMOVED_SYMBOL: &str = "❌";
|
||||
const WARNING_SYMBOL: &str = "⚠️";
|
||||
const STABLE_SYMBOL: &str = "✔️";
|
||||
const SPACER: &str = " ";
|
||||
|
||||
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>, linter: &Linter) {
|
||||
table_out.push_str("| Code | Name | Message | |");
|
||||
@@ -21,20 +26,33 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
table_out.push_str("| ---- | ---- | ------- | ------: |");
|
||||
table_out.push('\n');
|
||||
for rule in rules {
|
||||
let status_token = match rule.group() {
|
||||
RuleGroup::Removed => {
|
||||
format!("<span title='Rule has been removed'>{REMOVED_SYMBOL}</span>")
|
||||
}
|
||||
RuleGroup::Deprecated => {
|
||||
format!("<span title='Rule has been deprecated'>{WARNING_SYMBOL}</span>")
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
RuleGroup::Preview | RuleGroup::Nursery => {
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
}
|
||||
RuleGroup::Stable => {
|
||||
// A full opacity checkmark is a bit aggressive for indicating stable
|
||||
format!("<span title='Rule is stable' style='opacity: 0.6'>{STABLE_SYMBOL}</span>")
|
||||
}
|
||||
};
|
||||
|
||||
let fix_token = match rule.fixable() {
|
||||
FixAvailability::Always | FixAvailability::Sometimes => {
|
||||
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::None => {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
format!("<span title='Automatic fix not available' style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
};
|
||||
let preview_token = if rule.is_preview() || rule.is_nursery() {
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
let tokens = format!("{status_token} {fix_token}");
|
||||
|
||||
let rule_name = rule.as_ref();
|
||||
|
||||
@@ -48,9 +66,20 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
Cow::Borrowed(message)
|
||||
};
|
||||
|
||||
// Start and end of style spans
|
||||
let mut ss = "";
|
||||
let mut se = "";
|
||||
if rule.is_removed() {
|
||||
ss = "<span style='opacity: 0.5', title='This rule has been removed'>";
|
||||
se = "</span>";
|
||||
} else if rule.is_deprecated() {
|
||||
ss = "<span style='opacity: 0.8', title='This rule has been deprecated'>";
|
||||
se = "</span>";
|
||||
}
|
||||
|
||||
#[allow(clippy::or_fun_call)]
|
||||
table_out.push_str(&format!(
|
||||
"| {0}{1} {{ #{0}{1} }} | {2} | {3} | {4} |",
|
||||
"| {ss}{0}{1}{se} {{ #{0}{1} }} | {ss}{2}{se} | {ss}{3}{se} | {ss}{4}{se} |",
|
||||
linter.common_prefix(),
|
||||
linter.code_for_rule(rule).unwrap(),
|
||||
rule.explanation()
|
||||
@@ -58,7 +87,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
.then_some(format_args!("[{rule_name}](rules/{rule_name}.md)"))
|
||||
.unwrap_or(format_args!("{rule_name}")),
|
||||
message,
|
||||
status_token,
|
||||
tokens,
|
||||
));
|
||||
table_out.push('\n');
|
||||
}
|
||||
@@ -69,15 +98,33 @@ pub(crate) fn generate() -> String {
|
||||
// Generate the table string.
|
||||
let mut table_out = String::new();
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option."));
|
||||
table_out.push('\n');
|
||||
table_out.push_str("### Legend");
|
||||
table_out.push('\n');
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {PREVIEW_SYMBOL} emoji indicates that a rule is in [\"preview\"](faq.md#what-is-preview)."
|
||||
"{SPACER}{STABLE_SYMBOL}{SPACER} The rule is stable."
|
||||
));
|
||||
table_out.push('\n');
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{PREVIEW_SYMBOL}{SPACER} The rule is unstable and is in [\"preview\"](faq.md#what-is-preview)."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{WARNING_SYMBOL}{SPACER} The rule has been deprecated and will be removed in a future release."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{REMOVED_SYMBOL}{SPACER} The rule has been removed only the documentation is available."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{FIX_SYMBOL}{SPACER} The rule is automatically fixable by the `--fix` command-line option."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
table_out.push('\n');
|
||||
|
||||
for linter in Linter::iter() {
|
||||
|
||||
@@ -308,11 +308,8 @@ impl std::fmt::Debug for Token {
|
||||
/// assert_eq!(printed.as_code(), r#""Hello 'Ruff'""#);
|
||||
/// assert_eq!(printed.sourcemap(), [
|
||||
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(0) },
|
||||
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(7) },
|
||||
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(7) },
|
||||
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(13) },
|
||||
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(13) },
|
||||
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(14) },
|
||||
/// SourceMarker { source: TextSize::new(20), dest: TextSize::new(14) },
|
||||
/// ]);
|
||||
///
|
||||
@@ -328,24 +325,30 @@ pub struct SourcePosition(TextSize);
|
||||
|
||||
impl<Context> Format<Context> for SourcePosition {
|
||||
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
|
||||
if let Some(FormatElement::SourcePosition(last_position)) = f.buffer.elements().last() {
|
||||
if *last_position == self.0 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
f.write_element(FormatElement::SourcePosition(self.0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a text from a dynamic string with its optional start-position in the source document.
|
||||
/// Creates a text from a dynamic string.
|
||||
///
|
||||
/// This is done by allocating a new string internally.
|
||||
pub fn text(text: &str, position: Option<TextSize>) -> Text {
|
||||
pub fn text(text: &str) -> Text {
|
||||
debug_assert_no_newlines(text);
|
||||
|
||||
Text { text, position }
|
||||
Text { text }
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct Text<'a> {
|
||||
text: &'a str,
|
||||
position: Option<TextSize>,
|
||||
}
|
||||
|
||||
impl<Context> Format<Context> for Text<'_>
|
||||
@@ -353,10 +356,6 @@ where
|
||||
Context: FormatContext,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
|
||||
if let Some(source_position) = self.position {
|
||||
f.write_element(FormatElement::SourcePosition(source_position));
|
||||
}
|
||||
|
||||
f.write_element(FormatElement::Text {
|
||||
text: self.text.to_string().into_boxed_str(),
|
||||
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
|
||||
@@ -2286,7 +2285,7 @@ impl<Context, T> std::fmt::Debug for FormatWith<Context, T> {
|
||||
/// let mut join = f.join_with(&separator);
|
||||
///
|
||||
/// for item in &self.items {
|
||||
/// join.entry(&format_with(|f| write!(f, [text(item, None)])));
|
||||
/// join.entry(&format_with(|f| write!(f, [text(item)])));
|
||||
/// }
|
||||
/// join.finish()
|
||||
/// })),
|
||||
@@ -2371,7 +2370,7 @@ where
|
||||
/// let mut count = 0;
|
||||
///
|
||||
/// let value = format_once(|f| {
|
||||
/// write!(f, [text(&std::format!("Formatted {count}."), None)])
|
||||
/// write!(f, [text(&std::format!("Formatted {count}."))])
|
||||
/// });
|
||||
///
|
||||
/// format!(SimpleFormatContext::default(), [value]).expect("Formatting once works fine");
|
||||
|
||||
@@ -346,10 +346,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
}
|
||||
|
||||
FormatElement::SourcePosition(position) => {
|
||||
write!(
|
||||
f,
|
||||
[text(&std::format!("source_position({position:?})"), None)]
|
||||
)?;
|
||||
write!(f, [text(&std::format!("source_position({position:?})"))])?;
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
@@ -360,7 +357,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
write!(f, [token("best_fitting(")])?;
|
||||
|
||||
if *mode != BestFittingMode::FirstLine {
|
||||
write!(f, [text(&std::format!("mode: {mode:?}, "), None)])?;
|
||||
write!(f, [text(&std::format!("mode: {mode:?}, "))])?;
|
||||
}
|
||||
|
||||
write!(f, [token("[")])?;
|
||||
@@ -392,17 +389,14 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("<interned {index}>"), None),
|
||||
text(&std::format!("<interned {index}>")),
|
||||
space(),
|
||||
&&**interned,
|
||||
]
|
||||
)?;
|
||||
}
|
||||
Some(reference) => {
|
||||
write!(
|
||||
f,
|
||||
[text(&std::format!("<ref interned *{reference}>"), None)]
|
||||
)?;
|
||||
write!(f, [text(&std::format!("<ref interned *{reference}>"))])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +415,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("<END_TAG_WITHOUT_START<"),
|
||||
text(&std::format!("{:?}", tag.kind()), None),
|
||||
text(&std::format!("{:?}", tag.kind())),
|
||||
token(">>"),
|
||||
]
|
||||
)?;
|
||||
@@ -436,9 +430,9 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
token(")"),
|
||||
soft_line_break_or_space(),
|
||||
token("ERROR<START_END_TAG_MISMATCH<start: "),
|
||||
text(&std::format!("{start_kind:?}"), None),
|
||||
text(&std::format!("{start_kind:?}")),
|
||||
token(", end: "),
|
||||
text(&std::format!("{:?}", tag.kind()), None),
|
||||
text(&std::format!("{:?}", tag.kind())),
|
||||
token(">>")
|
||||
]
|
||||
)?;
|
||||
@@ -470,7 +464,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("align("),
|
||||
text(&count.to_string(), None),
|
||||
text(&count.to_string()),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -482,7 +476,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("line_suffix("),
|
||||
text(&std::format!("{reserved_width:?}"), None),
|
||||
text(&std::format!("{reserved_width:?}")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -499,11 +493,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = group.id() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -524,11 +514,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = id {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -561,7 +547,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("indent_if_group_breaks("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -581,11 +567,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = condition.group_id {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space()]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -595,7 +577,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("label("),
|
||||
text(&std::format!("\"{label_id:?}\""), None),
|
||||
text(&std::format!("\"{label_id:?}\"")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -664,7 +646,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
ContentArrayEnd,
|
||||
token(")"),
|
||||
soft_line_break_or_space(),
|
||||
text(&std::format!("<START_WITHOUT_END<{top:?}>>"), None),
|
||||
text(&std::format!("<START_WITHOUT_END<{top:?}>>")),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
@@ -807,7 +789,7 @@ impl Format<IrFormatContext<'_>> for Condition {
|
||||
f,
|
||||
[
|
||||
token("if_group_fits_on_line("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(")")
|
||||
]
|
||||
),
|
||||
@@ -816,7 +798,7 @@ impl Format<IrFormatContext<'_>> for Condition {
|
||||
f,
|
||||
[
|
||||
token("if_group_breaks("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(")")
|
||||
]
|
||||
),
|
||||
|
||||
@@ -32,7 +32,7 @@ pub trait MemoizeFormat<Context> {
|
||||
/// let value = self.value.get();
|
||||
/// self.value.set(value + 1);
|
||||
///
|
||||
/// write!(f, [text(&std::format!("Formatted {value} times."), None)])
|
||||
/// write!(f, [text(&std::format!("Formatted {value} times."))])
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -110,7 +110,7 @@ where
|
||||
/// write!(f, [
|
||||
/// token("Count:"),
|
||||
/// space(),
|
||||
/// text(&std::format!("{current}"), None),
|
||||
/// text(&std::format!("{current}")),
|
||||
/// hard_line_break()
|
||||
/// ])?;
|
||||
///
|
||||
|
||||
@@ -41,7 +41,7 @@ use std::marker::PhantomData;
|
||||
use std::num::{NonZeroU16, NonZeroU8, TryFromIntError};
|
||||
|
||||
use crate::format_element::document::Document;
|
||||
use crate::printer::{Printer, PrinterOptions, SourceMapGeneration};
|
||||
use crate::printer::{Printer, PrinterOptions};
|
||||
pub use arguments::{Argument, Arguments};
|
||||
pub use buffer::{
|
||||
Buffer, BufferExtensions, BufferSnapshot, Inspect, RemoveSoftLinesBuffer, VecBuffer,
|
||||
@@ -53,7 +53,7 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
|
||||
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
|
||||
pub use group_id::GroupId;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
|
||||
#[cfg_attr(
|
||||
@@ -269,7 +269,6 @@ impl FormatOptions for SimpleFormatOptions {
|
||||
line_width: self.line_width,
|
||||
indent_style: self.indent_style,
|
||||
indent_width: self.indent_width,
|
||||
source_map_generation: SourceMapGeneration::Enabled,
|
||||
..PrinterOptions::default()
|
||||
}
|
||||
}
|
||||
@@ -432,6 +431,129 @@ impl Printed {
|
||||
pub fn take_verbatim_ranges(&mut self) -> Vec<TextRange> {
|
||||
std::mem::take(&mut self.verbatim_ranges)
|
||||
}
|
||||
|
||||
/// Slices the formatted code to the sub-slices that covers the passed `source_range` in `source`.
|
||||
///
|
||||
/// The implementation uses the source map generated during formatting to find the closest range
|
||||
/// in the formatted document that covers `source_range` or more. The returned slice
|
||||
/// matches the `source_range` exactly (except indent, see below) if the formatter emits [`FormatElement::SourcePosition`] for
|
||||
/// the range's offsets.
|
||||
///
|
||||
/// ## Indentation
|
||||
/// The indentation before `source_range.start` is replaced with the indentation returned by the formatter
|
||||
/// to fix up incorrectly intended code.
|
||||
///
|
||||
/// Returns the entire document if the source map is empty.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `source_range` points to offsets that are not in the bounds of `source`.
|
||||
#[must_use]
|
||||
pub fn slice_range(self, source_range: TextRange, source: &str) -> PrintedRange {
|
||||
let mut start_marker: Option<SourceMarker> = None;
|
||||
let mut end_marker: Option<SourceMarker> = None;
|
||||
|
||||
// Note: The printer can generate multiple source map entries for the same source position.
|
||||
// For example if you have:
|
||||
// * token("a + b")
|
||||
// * `source_position(276)`
|
||||
// * `token(")")`
|
||||
// * `source_position(276)`
|
||||
// * `hard_line_break`
|
||||
// The printer uses the source position 276 for both the tokens `)` and the `\n` because
|
||||
// there were multiple `source_position` entries in the IR with the same offset.
|
||||
// This can happen if multiple nodes start or end at the same position. A common example
|
||||
// for this are expressions and expression statement that always end at the same offset.
|
||||
//
|
||||
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed
|
||||
// and depends on the emitted `IR`.
|
||||
// They are only guaranteed to be sorted in increasing order by their destination position.
|
||||
for marker in self.sourcemap {
|
||||
// Take the closest start marker, but skip over start_markers that have the same start.
|
||||
if marker.source <= source_range.start()
|
||||
&& !start_marker.is_some_and(|existing| existing.source >= marker.source)
|
||||
{
|
||||
start_marker = Some(marker);
|
||||
}
|
||||
|
||||
if marker.source >= source_range.end()
|
||||
&& !end_marker.is_some_and(|existing| existing.source <= marker.source)
|
||||
{
|
||||
end_marker = Some(marker);
|
||||
}
|
||||
}
|
||||
|
||||
let (source_start, formatted_start) = start_marker
|
||||
.map(|marker| (marker.source, marker.dest))
|
||||
.unwrap_or_default();
|
||||
|
||||
let (source_end, formatted_end) = end_marker
|
||||
.map_or((source.text_len(), self.code.text_len()), |marker| {
|
||||
(marker.source, marker.dest)
|
||||
});
|
||||
|
||||
let source_range = TextRange::new(source_start, source_end);
|
||||
let formatted_range = TextRange::new(formatted_start, formatted_end);
|
||||
|
||||
// Extend both ranges to include the indentation
|
||||
let source_range = extend_range_to_include_indent(source_range, source);
|
||||
let formatted_range = extend_range_to_include_indent(formatted_range, &self.code);
|
||||
|
||||
PrintedRange {
|
||||
code: self.code[formatted_range].to_string(),
|
||||
source_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends `range` backwards (by reducing `range.start`) to include any directly preceding whitespace (`\t` or ` `).
|
||||
///
|
||||
/// # Panics
|
||||
/// If `range.start` is out of `source`'s bounds.
|
||||
fn extend_range_to_include_indent(range: TextRange, source: &str) -> TextRange {
|
||||
let whitespace_len: TextSize = source[..usize::from(range.start())]
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| matches!(c, ' ' | '\t'))
|
||||
.map(TextLen::text_len)
|
||||
.sum();
|
||||
|
||||
TextRange::new(range.start() - whitespace_len, range.end())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct PrintedRange {
|
||||
code: String,
|
||||
source_range: TextRange,
|
||||
}
|
||||
|
||||
impl PrintedRange {
|
||||
pub fn new(code: String, source_range: TextRange) -> Self {
|
||||
Self { code, source_range }
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
code: String::new(),
|
||||
source_range: TextRange::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The formatted code.
|
||||
pub fn as_code(&self) -> &str {
|
||||
&self.code
|
||||
}
|
||||
|
||||
/// The range the formatted code corresponds to in the source document.
|
||||
pub fn source_range(&self) -> TextRange {
|
||||
self.source_range
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_code(self, code: String) -> Self {
|
||||
Self { code, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Public return type of the formatter
|
||||
@@ -453,7 +575,7 @@ pub type FormatResult<F> = Result<F, FormatError>;
|
||||
/// impl Format<SimpleFormatContext> for Paragraph {
|
||||
/// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
|
||||
/// write!(f, [
|
||||
/// text(&self.0, None),
|
||||
/// text(&self.0),
|
||||
/// hard_line_break(),
|
||||
/// ])
|
||||
/// }
|
||||
|
||||
@@ -4,7 +4,7 @@ use drop_bomb::DebugDropBomb;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
pub use printer_options::*;
|
||||
use ruff_text_size::{Ranged, TextLen, TextSize};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::format_element::document::Document;
|
||||
use crate::format_element::tag::{Condition, GroupMode};
|
||||
@@ -60,7 +60,10 @@ impl<'a> Printer<'a> {
|
||||
document: &'a Document,
|
||||
indent: u16,
|
||||
) -> PrintResult<Printed> {
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new(Indention::Level(indent)));
|
||||
let indentation = Indention::Level(indent);
|
||||
self.state.pending_indent = indentation;
|
||||
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new(indentation));
|
||||
let mut queue: PrintQueue<'a> = PrintQueue::new(document.as_ref());
|
||||
|
||||
loop {
|
||||
@@ -73,6 +76,9 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Push any pending marker
|
||||
self.push_marker();
|
||||
|
||||
Ok(Printed::new(
|
||||
self.state.buffer,
|
||||
None,
|
||||
@@ -94,42 +100,38 @@ impl<'a> Printer<'a> {
|
||||
let args = stack.top();
|
||||
|
||||
match element {
|
||||
FormatElement::Space => self.print_text(Text::Token(" "), None),
|
||||
FormatElement::Token { text } => self.print_text(Text::Token(text), None),
|
||||
FormatElement::Text { text, text_width } => self.print_text(
|
||||
Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
},
|
||||
None,
|
||||
),
|
||||
FormatElement::Space => self.print_text(Text::Token(" ")),
|
||||
FormatElement::Token { text } => self.print_text(Text::Token(text)),
|
||||
FormatElement::Text { text, text_width } => self.print_text(Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
}),
|
||||
FormatElement::SourceCodeSlice { slice, text_width } => {
|
||||
let text = slice.text(self.source_code);
|
||||
self.print_text(
|
||||
Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
},
|
||||
Some(slice.range()),
|
||||
);
|
||||
self.print_text(Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
});
|
||||
}
|
||||
FormatElement::Line(line_mode) => {
|
||||
if args.mode().is_flat()
|
||||
&& matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace)
|
||||
{
|
||||
if line_mode == &LineMode::SoftOrSpace {
|
||||
self.print_text(Text::Token(" "), None);
|
||||
self.print_text(Text::Token(" "));
|
||||
}
|
||||
} else if self.state.line_suffixes.has_pending() {
|
||||
self.flush_line_suffixes(queue, stack, Some(element));
|
||||
} else {
|
||||
// Only print a newline if the current line isn't already empty
|
||||
if self.state.line_width > 0 {
|
||||
self.push_marker();
|
||||
self.print_char('\n');
|
||||
}
|
||||
|
||||
// Print a second line break if this is an empty line
|
||||
if line_mode == &LineMode::Empty {
|
||||
self.push_marker();
|
||||
self.print_char('\n');
|
||||
}
|
||||
|
||||
@@ -142,8 +144,11 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
|
||||
FormatElement::SourcePosition(position) => {
|
||||
self.state.source_position = *position;
|
||||
self.push_marker();
|
||||
// The printer defers printing indents until the next text
|
||||
// is printed. Pushing the marker now would mean that the
|
||||
// mapped range includes the indent range, which we don't want.
|
||||
// Queue the source map position and emit it when printing the next character
|
||||
self.state.pending_source_position = Some(*position);
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
@@ -435,7 +440,7 @@ impl<'a> Printer<'a> {
|
||||
Ok(print_mode)
|
||||
}
|
||||
|
||||
fn print_text(&mut self, text: Text, source_range: Option<TextRange>) {
|
||||
fn print_text(&mut self, text: Text) {
|
||||
if !self.state.pending_indent.is_empty() {
|
||||
let (indent_char, repeat_count) = match self.options.indent_style() {
|
||||
IndentStyle::Tab => ('\t', 1),
|
||||
@@ -458,19 +463,6 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Insert source map markers before and after the token
|
||||
//
|
||||
// If the token has source position information the start marker
|
||||
// will use the start position of the original token, and the end
|
||||
// marker will use that position + the text length of the token
|
||||
//
|
||||
// If the token has no source position (was created by the formatter)
|
||||
// both the start and end marker will use the last known position
|
||||
// in the input source (from state.source_position)
|
||||
if let Some(range) = source_range {
|
||||
self.state.source_position = range.start();
|
||||
}
|
||||
|
||||
self.push_marker();
|
||||
|
||||
match text {
|
||||
@@ -493,29 +485,24 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(range) = source_range {
|
||||
self.state.source_position = range.end();
|
||||
}
|
||||
|
||||
self.push_marker();
|
||||
}
|
||||
|
||||
fn push_marker(&mut self) {
|
||||
if self.options.source_map_generation.is_disabled() {
|
||||
let Some(source_position) = self.state.pending_source_position.take() else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let marker = SourceMarker {
|
||||
source: self.state.source_position,
|
||||
source: source_position,
|
||||
dest: self.state.buffer.text_len(),
|
||||
};
|
||||
|
||||
if let Some(last) = self.state.source_markers.last() {
|
||||
if last != &marker {
|
||||
self.state.source_markers.push(marker);
|
||||
}
|
||||
} else {
|
||||
if self
|
||||
.state
|
||||
.source_markers
|
||||
.last()
|
||||
.map_or(true, |last| last != &marker)
|
||||
{
|
||||
self.state.source_markers.push(marker);
|
||||
}
|
||||
}
|
||||
@@ -887,7 +874,7 @@ enum FillPairLayout {
|
||||
struct PrinterState<'a> {
|
||||
buffer: String,
|
||||
source_markers: Vec<SourceMarker>,
|
||||
source_position: TextSize,
|
||||
pending_source_position: Option<TextSize>,
|
||||
pending_indent: Indention,
|
||||
measured_group_fits: bool,
|
||||
line_width: u32,
|
||||
@@ -1742,7 +1729,7 @@ a",
|
||||
let result = format_with_options(
|
||||
&format_args![
|
||||
token("function main() {"),
|
||||
block_indent(&text("let x = `This is a multiline\nstring`;", None)),
|
||||
block_indent(&text("let x = `This is a multiline\nstring`;")),
|
||||
token("}"),
|
||||
hard_line_break()
|
||||
],
|
||||
@@ -1759,7 +1746,7 @@ a",
|
||||
fn it_breaks_a_group_if_a_string_contains_a_newline() {
|
||||
let result = format(&FormatArrayElements {
|
||||
items: vec![
|
||||
&text("`This is a string spanning\ntwo lines`", None),
|
||||
&text("`This is a string spanning\ntwo lines`"),
|
||||
&token("\"b\""),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,10 +14,6 @@ pub struct PrinterOptions {
|
||||
|
||||
/// The type of line ending to apply to the printed input
|
||||
pub line_ending: LineEnding,
|
||||
|
||||
/// Whether the printer should build a source map that allows mapping positions in the source document
|
||||
/// to positions in the formatted document.
|
||||
pub source_map_generation: SourceMapGeneration,
|
||||
}
|
||||
|
||||
impl<'a, O> From<&'a O> for PrinterOptions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.15"
|
||||
version = "0.2.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -85,6 +85,8 @@ tempfile = { workspace = true }
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars"]
|
||||
# Enables rules for internal integration tests
|
||||
test-rules = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -13,3 +13,11 @@ s = f"{set([f(x) for x in 'ab'])}"
|
||||
|
||||
s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
|
||||
s = set( # comment
|
||||
[x for x in range(3)]
|
||||
)
|
||||
|
||||
s = set([ # comment
|
||||
x for x in range(3)
|
||||
])
|
||||
|
||||
@@ -20,3 +20,10 @@ f"{dict(x='y') | dict(y='z')}"
|
||||
f"{ dict(x='y') | dict(y='z') }"
|
||||
f"a {dict(x='y') | dict(y='z')} b"
|
||||
f"a { dict(x='y') | dict(y='z') } b"
|
||||
|
||||
dict(
|
||||
# comment
|
||||
)
|
||||
|
||||
tuple( # comment
|
||||
)
|
||||
|
||||
@@ -8,3 +8,11 @@ t4 = tuple([
|
||||
t5 = tuple(
|
||||
(1, 2)
|
||||
)
|
||||
|
||||
tuple( # comment
|
||||
[1, 2]
|
||||
)
|
||||
|
||||
tuple([ # comment
|
||||
1, 2
|
||||
])
|
||||
|
||||
@@ -2,3 +2,12 @@ l1 = list([1, 2])
|
||||
l2 = list((1, 2))
|
||||
l3 = list([])
|
||||
l4 = list(())
|
||||
|
||||
|
||||
list( # comment
|
||||
[1, 2]
|
||||
)
|
||||
|
||||
list([ # comment
|
||||
1, 2
|
||||
])
|
||||
|
||||
@@ -207,3 +207,23 @@ class Repro:
|
||||
def stub(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
|
||||
class Repro(Protocol[int]):
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
|
||||
class Repro[int](Protocol):
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
@@ -192,3 +192,49 @@ elif x == 2:
|
||||
y = "b"
|
||||
else:
|
||||
y = "c"
|
||||
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/9732
|
||||
def sb(self):
|
||||
if self._sb is not None: return self._sb
|
||||
else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
@@ -1,16 +0,0 @@
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH010_1.py
vendored
Normal file
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH010_1.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH010
|
||||
x: ("int" | str) | "bool" # TCH010
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH010
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH010
|
||||
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH010_2.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH010_2.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH010
|
||||
x: ("int" | str) | "bool" # TCH010
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH010
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH010
|
||||
15
crates/ruff_linter/resources/test/fixtures/pycodestyle/W291.py
vendored
Normal file
15
crates/ruff_linter/resources/test/fixtures/pycodestyle/W291.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
'''trailing whitespace
|
||||
inside a multiline string'''
|
||||
|
||||
f'''trailing whitespace
|
||||
inside a multiline f-string'''
|
||||
|
||||
# Trailing whitespace after `{`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
|
||||
# Trailing whitespace after `2`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
@@ -1,9 +0,0 @@
|
||||
from ast import literal_eval
|
||||
|
||||
eval("3 + 4")
|
||||
|
||||
literal_eval({1: 2})
|
||||
|
||||
|
||||
def fn() -> None:
|
||||
eval("3 + 4")
|
||||
@@ -1,11 +0,0 @@
|
||||
def eval(content: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
eval("3 + 4")
|
||||
|
||||
literal_eval({1: 2})
|
||||
|
||||
|
||||
def fn() -> None:
|
||||
eval("3 + 4")
|
||||
@@ -1,10 +0,0 @@
|
||||
import logging
|
||||
import warnings
|
||||
from warnings import warn
|
||||
|
||||
warnings.warn("this is ok")
|
||||
warn("by itself is also ok")
|
||||
logging.warning("this is fine")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("this is fine")
|
||||
@@ -1,8 +0,0 @@
|
||||
import logging
|
||||
from logging import warn
|
||||
|
||||
logging.warn("this is not ok")
|
||||
warn("not ok")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warn("this is not ok")
|
||||
@@ -1,73 +0,0 @@
|
||||
# OK
|
||||
|
||||
1<2 and 'b' and 'c'
|
||||
|
||||
1<2 or 'a' and 'b'
|
||||
|
||||
1<2 and 'a'
|
||||
|
||||
1<2 or 'a'
|
||||
|
||||
2>1
|
||||
|
||||
1<2 and 'a' or 'b' and 'c'
|
||||
|
||||
1<2 and 'a' or 'b' or 'c'
|
||||
|
||||
1<2 and 'a' or 'b' or 'c' or (lambda x: x+1)
|
||||
|
||||
1<2 and 'a' or 'b' or (lambda x: x+1) or 'c'
|
||||
|
||||
default = 'default'
|
||||
if (not isinstance(default, bool) and isinstance(default, int)) \
|
||||
or (isinstance(default, str) and default):
|
||||
pass
|
||||
|
||||
docid, token = None, None
|
||||
(docid is None and token is None) or (docid is not None and token is not None)
|
||||
|
||||
vendor, os_version = 'darwin', '14'
|
||||
vendor == "debian" and os_version in ["12"] or vendor == "ubuntu" and os_version in []
|
||||
|
||||
# Don't emit if the parent is an `if` statement.
|
||||
if (task_id in task_dict and task_dict[task_id] is not task) \
|
||||
or task_id in used_group_ids:
|
||||
pass
|
||||
|
||||
no_target, is_x64, target = True, False, 'target'
|
||||
if (no_target and not is_x64) or target == 'ARM_APPL_RUST_TARGET':
|
||||
pass
|
||||
|
||||
# Don't emit if the parent is a `bool_op` expression.
|
||||
isinstance(val, str) and ((len(val) == 7 and val[0] == "#") or val in enums.NamedColor)
|
||||
|
||||
# Errors
|
||||
|
||||
1<2 and 'a' or 'b'
|
||||
|
||||
(lambda x: x+1) and 'a' or 'b'
|
||||
|
||||
'a' and (lambda x: x+1) or 'orange'
|
||||
|
||||
val = '#0000FF'
|
||||
(len(val) == 7 and val[0] == "#") or val in {'green'}
|
||||
|
||||
marker = 'marker'
|
||||
isinstance(marker, dict) and 'field' in marker or marker in {}
|
||||
|
||||
def has_oranges(oranges, apples=None) -> bool:
|
||||
return apples and False or oranges
|
||||
|
||||
[x for x in l if a and b or c]
|
||||
|
||||
{x: y for x in l if a and b or c}
|
||||
|
||||
{x for x in l if a and b or c}
|
||||
|
||||
new_list = [
|
||||
x
|
||||
for sublist in all_lists
|
||||
if a and b or c
|
||||
for x in sublist
|
||||
if (isinstance(operator, list) and x in operator) or x != operator
|
||||
]
|
||||
@@ -6,8 +6,9 @@ dictVarBad = os.getenv("AAA", {"a", 7}) # [invalid-envvar-default]
|
||||
print(os.getenv("TEST", False)) # [invalid-envvar-default]
|
||||
os.getenv("AA", "GOOD")
|
||||
os.getenv("AA", f"GOOD")
|
||||
os.getenv("AA", "GOOD" + "BAD")
|
||||
os.getenv("AA", "GOOD" + "BAR")
|
||||
os.getenv("AA", "GOOD" + 1)
|
||||
os.getenv("AA", "GOOD %s" % "BAD")
|
||||
os.getenv("AA", "GOOD %s" % "BAR")
|
||||
os.getenv("B", Z)
|
||||
|
||||
os.getenv("AA", "GOOD" if Z else "BAR")
|
||||
os.getenv("AA", 1 if Z else "BAR") # [invalid-envvar-default]
|
||||
|
||||
@@ -30,6 +30,11 @@ class Thing:
|
||||
def do_thing(self, item):
|
||||
return object.__getattribute__(self, item) # PLC2801
|
||||
|
||||
def use_descriptor(self, item):
|
||||
item.__get__(self, type(self)) # OK
|
||||
item.__set__(self, 1) # OK
|
||||
item.__delete__(self) # OK
|
||||
|
||||
|
||||
blah = lambda: {"a": 1}.__delitem__("a") # OK
|
||||
|
||||
|
||||
@@ -215,3 +215,13 @@ if sys.version_info[:2] > (3,13):
|
||||
|
||||
if sys.version_info[:3] > (3,13):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info > (3,0):
|
||||
f"this is\
|
||||
allowed too"
|
||||
|
||||
f"""the indentation on
|
||||
this line is significant"""
|
||||
|
||||
"this is\
|
||||
allowed too"
|
||||
|
||||
58
crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py
vendored
Normal file
58
crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import abc
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
|
||||
# Errors
|
||||
|
||||
class A0(metaclass=abc.ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
|
||||
|
||||
class A1(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
|
||||
|
||||
class B0:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__()
|
||||
|
||||
|
||||
class B1:
|
||||
pass
|
||||
|
||||
|
||||
class A2(B0, B1, metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
|
||||
|
||||
class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
# OK
|
||||
|
||||
class Meta(type):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super().__new__(cls, *args)
|
||||
|
||||
|
||||
class A4(metaclass=Meta, no_metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
|
||||
|
||||
class A5(metaclass=Meta):
|
||||
pass
|
||||
|
||||
|
||||
class A6(abc.ABC):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
|
||||
|
||||
class A7(B0, abc.ABC, B1):
|
||||
@abstractmethod
|
||||
def foo(self): pass
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
class Klass:
|
||||
__slots__ = ["d", "c", "b", "a"] # a comment that is untouched
|
||||
__match_args__ = ("d", "c", "b", "a")
|
||||
__slots__ = ("d", "c", "b", "a")
|
||||
|
||||
# Quoting style is retained,
|
||||
# but unnecessary parens are not
|
||||
__slots__: set = {'b', "c", ((('a')))}
|
||||
# Trailing commas are also not retained for single-line definitions
|
||||
# (but they are in multiline definitions)
|
||||
__match_args__: tuple = ("b", "c", "a",)
|
||||
__slots__: tuple = ("b", "c", "a",)
|
||||
|
||||
class Klass2:
|
||||
if bool():
|
||||
@@ -19,7 +19,7 @@ class Klass2:
|
||||
else:
|
||||
__slots__ = "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens)
|
||||
|
||||
__match_args__: list[str] = ["the", "three", "little", "pigs"]
|
||||
__slots__: list[str] = ["the", "three", "little", "pigs"]
|
||||
__slots__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple")
|
||||
# we use natural sort,
|
||||
# not alphabetical sort or "isort-style" sort
|
||||
@@ -37,7 +37,7 @@ class Klass3:
|
||||
# a comment regarding 'a0':
|
||||
"a0"
|
||||
)
|
||||
__match_args__ = [
|
||||
__slots__ = [
|
||||
"d",
|
||||
"c", # a comment regarding 'c'
|
||||
"b",
|
||||
@@ -61,7 +61,7 @@ class Klass4:
|
||||
) # comment6
|
||||
# comment7
|
||||
|
||||
__match_args__ = [ # comment0
|
||||
__slots__ = [ # comment0
|
||||
# comment1
|
||||
# comment2
|
||||
"dx", "cx", "bx", "ax" # comment3
|
||||
@@ -139,7 +139,7 @@ class SlotUser:
|
||||
'distance': 'measured in kilometers'}
|
||||
|
||||
class Klass5:
|
||||
__match_args__ = (
|
||||
__slots__ = (
|
||||
"look",
|
||||
(
|
||||
"a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item"
|
||||
@@ -194,14 +194,14 @@ class BezierBuilder4:
|
||||
|
||||
class Klass6:
|
||||
__slots__ = ()
|
||||
__match_args__ = []
|
||||
__slots__ = []
|
||||
__slots__ = ("single_item",)
|
||||
__match_args__ = (
|
||||
__slots__ = (
|
||||
"single_item_multiline",
|
||||
)
|
||||
__slots__ = {"single_item",}
|
||||
__slots__ = {"single_item_no_trailing_comma": "docs for that"}
|
||||
__match_args__ = [
|
||||
__slots__ = [
|
||||
"single_item_multiline_no_trailing_comma"
|
||||
]
|
||||
__slots__ = ("not_a_tuple_just_a_string")
|
||||
@@ -218,11 +218,11 @@ class Klass6:
|
||||
|
||||
__slots__ = ("b", "a", "e", "d")
|
||||
__slots__ = ["b", "a", "e", "d"]
|
||||
__match_args__ = ["foo", "bar", "antipasti"]
|
||||
__slots__ = ["foo", "bar", "antipasti"]
|
||||
|
||||
class Klass6:
|
||||
__slots__ = (9, 8, 7)
|
||||
__match_args__ = ( # This is just an empty tuple,
|
||||
__slots__ = ( # This is just an empty tuple,
|
||||
# but,
|
||||
# it's very well
|
||||
) # documented
|
||||
@@ -245,10 +245,10 @@ class Klass6:
|
||||
__slots__ = [
|
||||
()
|
||||
]
|
||||
__match_args__ = (
|
||||
__slots__ = (
|
||||
()
|
||||
)
|
||||
__match_args__ = (
|
||||
__slots__ = (
|
||||
[]
|
||||
)
|
||||
__slots__ = (
|
||||
@@ -257,12 +257,9 @@ class Klass6:
|
||||
__slots__ = (
|
||||
[],
|
||||
)
|
||||
__match_args__ = (
|
||||
__slots__ = (
|
||||
"foo", [], "bar"
|
||||
)
|
||||
__match_args__ = [
|
||||
__slots__ = [
|
||||
"foo", (), "bar"
|
||||
]
|
||||
|
||||
__match_args__ = {"a", "set", "for", "__match_args__", "is invalid"}
|
||||
__match_args__ = {"this": "is", "also": "invalid"}
|
||||
|
||||
85
crates/ruff_linter/resources/test/fixtures/ruff/RUF027.py
vendored
Normal file
85
crates/ruff_linter/resources/test/fixtures/ruff/RUF027.py
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
val = 2
|
||||
|
||||
def simple_cases():
|
||||
a = 4
|
||||
b = "{a}" # RUF027
|
||||
c = "{a} {b} f'{val}' " # RUF027
|
||||
|
||||
def escaped_string():
|
||||
a = 4
|
||||
b = "escaped string: {{ brackets surround me }}" # RUF027
|
||||
|
||||
def raw_string():
|
||||
a = 4
|
||||
b = r"raw string with formatting: {a}" # RUF027
|
||||
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
|
||||
|
||||
def print_name(name: str):
|
||||
a = 4
|
||||
print("Hello, {name}!") # RUF027
|
||||
print("The test value we're using today is {a}") # RUF027
|
||||
|
||||
def do_nothing(a):
|
||||
return a
|
||||
|
||||
def nested_funcs():
|
||||
a = 4
|
||||
print(do_nothing(do_nothing("{a}"))) # RUF027
|
||||
|
||||
def tripled_quoted():
|
||||
a = 4
|
||||
c = a
|
||||
single_line = """ {a} """ # RUF027
|
||||
# RUF027
|
||||
multi_line = a = """b { # comment
|
||||
c} d
|
||||
"""
|
||||
|
||||
def single_quoted_multi_line():
|
||||
a = 4
|
||||
# RUF027
|
||||
b = " {\
|
||||
a} \
|
||||
"
|
||||
|
||||
def implicit_concat():
|
||||
a = 4
|
||||
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
|
||||
print(f"{a}" "{a}" f"{b}") # RUF027
|
||||
|
||||
def escaped_chars():
|
||||
a = 4
|
||||
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
|
||||
|
||||
def alternative_formatter(src, **kwargs):
|
||||
src.format(**kwargs)
|
||||
|
||||
def format2(src, *args):
|
||||
pass
|
||||
|
||||
# These should not cause an RUF027 message
|
||||
def negative_cases():
|
||||
a = 4
|
||||
positive = False
|
||||
"""{a}"""
|
||||
"don't format: {a}"
|
||||
c = """ {b} """
|
||||
d = "bad variable: {invalid}"
|
||||
e = "incorrect syntax: {}"
|
||||
json = "{ positive: false }"
|
||||
json2 = "{ 'positive': false }"
|
||||
json3 = "{ 'positive': 'false' }"
|
||||
alternative_formatter("{a}", a = 5)
|
||||
formatted = "{a}".fmt(a = 7)
|
||||
print(do_nothing("{a}".format(a=3)))
|
||||
print(do_nothing(alternative_formatter("{a}", a = 5)))
|
||||
print(format(do_nothing("{a}"), a = 5))
|
||||
print("{a}".to_upper())
|
||||
print(do_nothing("{a}").format(a = "Test"))
|
||||
print(do_nothing("{a}").format2(a))
|
||||
|
||||
a = 4
|
||||
|
||||
"always ignore this: {a}"
|
||||
|
||||
print("but don't ignore this: {val}") # RUF027
|
||||
@@ -1,40 +0,0 @@
|
||||
"""
|
||||
Violation:
|
||||
Reraise without using 'from'
|
||||
"""
|
||||
|
||||
|
||||
class MyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def func():
|
||||
try:
|
||||
a = 1
|
||||
except Exception:
|
||||
raise MyException()
|
||||
|
||||
|
||||
def func():
|
||||
try:
|
||||
a = 1
|
||||
except Exception:
|
||||
if True:
|
||||
raise MyException()
|
||||
|
||||
|
||||
def good():
|
||||
try:
|
||||
a = 1
|
||||
except MyException as e:
|
||||
raise e # This is verbose violation, shouldn't trigger no cause
|
||||
except Exception:
|
||||
raise # Just re-raising don't need 'from'
|
||||
|
||||
|
||||
def good():
|
||||
try:
|
||||
from mod import f
|
||||
except ImportError:
|
||||
def f():
|
||||
raise MyException() # Raising within a new scope is fine
|
||||
@@ -256,25 +256,23 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
if let Some(import) = binding.as_any_import() {
|
||||
if let Some(source) = binding.source {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let statement = checker.semantic().statement(source);
|
||||
let parent = checker.semantic().parent_statement(source);
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once(import.member_name().as_ref()),
|
||||
statement,
|
||||
parent,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(source),
|
||||
)))
|
||||
});
|
||||
}
|
||||
if let Some(import) = binding.as_any_import() {
|
||||
if let Some(source) = binding.source {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let statement = checker.semantic().statement(source);
|
||||
let parent = checker.semantic().parent_statement(source);
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once(import.member_name().as_ref()),
|
||||
statement,
|
||||
parent,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(source),
|
||||
)))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use ruff_python_ast::str::raw_contents_range;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
|
||||
use ruff_python_semantic::{
|
||||
BindingKind, ContextualizedDefinition, Definition, Export, Member, MemberKind,
|
||||
};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::docstrings::Docstring;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle};
|
||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle, pylint};
|
||||
use crate::{docstrings, warn_user};
|
||||
|
||||
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
|
||||
@@ -31,6 +33,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
]);
|
||||
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
|
||||
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
|
||||
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName);
|
||||
let enforce_docstrings = checker.any_enabled(&[
|
||||
Rule::BlankLineAfterLastSection,
|
||||
Rule::BlankLineAfterSummary,
|
||||
@@ -80,7 +83,12 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
Rule::UndocumentedPublicPackage,
|
||||
]);
|
||||
|
||||
if !enforce_annotations && !enforce_docstrings && !enforce_stubs && !enforce_stubs_and_runtime {
|
||||
if !enforce_annotations
|
||||
&& !enforce_docstrings
|
||||
&& !enforce_stubs
|
||||
&& !enforce_stubs_and_runtime
|
||||
&& !enforce_dunder_method
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,6 +155,19 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
|
||||
// pylint
|
||||
if enforce_dunder_method {
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
if let Definition::Member(Member {
|
||||
kind: MemberKind::Method(method),
|
||||
..
|
||||
}) = definition
|
||||
{
|
||||
pylint::rules::bad_dunder_method_name(checker, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pydocstyle
|
||||
if enforce_docstrings {
|
||||
if pydocstyle::helpers::should_ignore_definition(
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
flake8_bandit, flake8_blind_except, flake8_bugbear, flake8_builtins, pycodestyle, pylint,
|
||||
tryceratops,
|
||||
};
|
||||
|
||||
/// Run lint rules over an [`ExceptHandler`] syntax node.
|
||||
@@ -66,9 +65,6 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &mut Check
|
||||
if checker.enabled(Rule::ExceptWithNonExceptionClasses) {
|
||||
flake8_bugbear::rules::except_with_non_exception_classes(checker, except_handler);
|
||||
}
|
||||
if checker.enabled(Rule::ReraiseNoCause) {
|
||||
tryceratops::rules::reraise_no_cause(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::BinaryOpException) {
|
||||
pylint::rules::binary_op_exception(checker, except_handler);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ use crate::rules::{
|
||||
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
|
||||
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_type_checking, flake8_use_pathlib,
|
||||
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
|
||||
refurb, ruff,
|
||||
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
@@ -320,7 +319,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
numpy::rules::numpy_2_0_deprecation(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::DeprecatedMockImport) {
|
||||
pyupgrade::rules::deprecated_mock_attribute(checker, expr);
|
||||
pyupgrade::rules::deprecated_mock_attribute(checker, attribute);
|
||||
}
|
||||
if checker.enabled(Rule::SixPY3) {
|
||||
flake8_2020::rules::name_or_attribute(checker, expr);
|
||||
@@ -337,7 +336,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UndocumentedWarn) {
|
||||
flake8_logging::rules::undocumented_warn(checker, expr);
|
||||
}
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
if checker.enabled(Rule::PandasUseOfDotValues) {
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
}
|
||||
}
|
||||
Expr::Call(
|
||||
call @ ast::ExprCall {
|
||||
@@ -637,14 +638,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_bandit::rules::tarfile_unsafe_members(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorList) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_list(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_generator_list(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorSet) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_generator_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorDict) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_dict(
|
||||
@@ -652,9 +649,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListComprehensionSet) {
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListComprehensionDict) {
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_dict(
|
||||
@@ -662,9 +657,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralSet) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralDict) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_dict(
|
||||
@@ -674,27 +667,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryCollectionCall) {
|
||||
flake8_comprehensions::rules::unnecessary_collection_call(
|
||||
checker,
|
||||
expr,
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
call,
|
||||
&checker.settings.flake8_comprehensions,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinTupleCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinListCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_list_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinDictCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListCall) {
|
||||
flake8_comprehensions::rules::unnecessary_list_call(checker, expr, func, args);
|
||||
@@ -773,12 +757,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::CallDateFromtimestamp) {
|
||||
flake8_datetimez::rules::call_date_fromtimestamp(checker, func, expr.range());
|
||||
}
|
||||
if checker.enabled(Rule::Eval) {
|
||||
pygrep_hooks::rules::no_eval(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::DeprecatedLogWarn) {
|
||||
pygrep_hooks::rules::deprecated_log_warn(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryDirectLambdaCall) {
|
||||
pylint::rules::unnecessary_direct_lambda_call(checker, expr, func);
|
||||
}
|
||||
@@ -983,7 +961,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnsortedDunderAll) {
|
||||
ruff::rules::sort_dunder_all_extend_call(checker, call);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::DefaultFactoryKwarg) {
|
||||
ruff::rules::default_factory_kwarg(checker, call);
|
||||
}
|
||||
@@ -1048,6 +1025,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_literal);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::MissingFStringSyntax) {
|
||||
for string_literal in value.literals() {
|
||||
ruff::rules::missing_fstring_syntax(
|
||||
&mut checker.diagnostics,
|
||||
string_literal,
|
||||
checker.locator,
|
||||
&checker.semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
@@ -1319,12 +1306,22 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::math_constant(checker, number_literal);
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => {
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_part in value {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::MissingFStringSyntax) {
|
||||
for string_literal in value.as_slice() {
|
||||
ruff::rules::missing_fstring_syntax(
|
||||
&mut checker.diagnostics,
|
||||
string_literal,
|
||||
checker.locator,
|
||||
&checker.semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::IfExp(
|
||||
if_exp @ ast::ExprIfExp {
|
||||
@@ -1446,7 +1443,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::StaticKeyDictComprehension) {
|
||||
ruff::rules::static_key_dict_comprehension(checker, dict_comp);
|
||||
flake8_bugbear::rules::static_key_dict_comprehension(checker, dict_comp);
|
||||
}
|
||||
}
|
||||
Expr::GeneratorExp(
|
||||
@@ -1508,9 +1505,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::RepeatedEqualityComparison) {
|
||||
pylint::rules::repeated_equality_comparison(checker, bool_op);
|
||||
}
|
||||
if checker.enabled(Rule::AndOrTernary) {
|
||||
pylint::rules::and_or_ternary(checker, bool_op);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryKeyCheck) {
|
||||
ruff::rules::unnecessary_key_check(checker, expr);
|
||||
}
|
||||
|
||||
@@ -513,8 +513,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SingleStringSlots) {
|
||||
pylint::rules::single_string_slots(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
pylint::rules::bad_dunder_method_name(checker, body);
|
||||
if checker.enabled(Rule::MetaClassABCMeta) {
|
||||
refurb::rules::metaclass_abcmeta(checker, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
|
||||
@@ -197,7 +197,7 @@ impl<'a> Checker<'a> {
|
||||
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
|
||||
|
||||
// Invert the quote character, if it's a single quote.
|
||||
match *trailing_quote {
|
||||
match trailing_quote {
|
||||
"'" => Some(Quote::Double),
|
||||
"\"" => Some(Quote::Single),
|
||||
_ => None,
|
||||
@@ -342,21 +342,13 @@ where
|
||||
|| helpers::is_assignment_to_a_dunder(stmt)
|
||||
|| helpers::in_nested_block(self.semantic.current_statements())
|
||||
|| imports::is_matplotlib_activation(stmt, self.semantic())
|
||||
|| self.settings.preview.is_enabled()
|
||||
&& imports::is_sys_path_modification(stmt, self.semantic()))
|
||||
|| imports::is_sys_path_modification(stmt, self.semantic()))
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track each top-level import, to guide import insertions.
|
||||
if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the flags prior to any further descent, so that we can restore them after visiting
|
||||
// the node.
|
||||
let flags_snapshot = self.semantic.flags;
|
||||
@@ -372,14 +364,22 @@ where
|
||||
self.handle_node_load(target);
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if alias.name.contains('.') && alias.asname.is_none() {
|
||||
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
|
||||
// "foo.bar".
|
||||
let name = alias.name.split('.').next().unwrap();
|
||||
// Given `import foo.bar`, `module` would be "foo", and `call_path` would be
|
||||
// `["foo", "bar"]`.
|
||||
let module = alias.name.split('.').next().unwrap();
|
||||
|
||||
// Mark the top-level module as "seen" by the semantic model.
|
||||
self.semantic.add_module(module);
|
||||
|
||||
if alias.asname.is_none() && alias.name.contains('.') {
|
||||
let call_path: Box<[&str]> = alias.name.split('.').collect();
|
||||
self.add_binding(
|
||||
name,
|
||||
module,
|
||||
alias.identifier(),
|
||||
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
|
||||
BindingFlags::EXTERNAL,
|
||||
@@ -414,8 +414,20 @@ where
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
|
||||
let module = module.as_deref();
|
||||
let level = *level;
|
||||
|
||||
// Mark the top-level module as "seen" by the semantic model.
|
||||
if level.map_or(true, |level| level == 0) {
|
||||
if let Some(module) = module.and_then(|module| module.split('.').next()) {
|
||||
self.semantic.add_module(module);
|
||||
}
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if let Some("__future__") = module {
|
||||
let name = alias.asname.as_ref().unwrap_or(&alias.name);
|
||||
|
||||
@@ -52,6 +52,11 @@ pub enum RuleGroup {
|
||||
Stable,
|
||||
/// The rule is unstable, and preview mode must be enabled for usage.
|
||||
Preview,
|
||||
/// The rule has been deprecated, warnings will be displayed during selection in stable
|
||||
/// and errors will be raised if used with preview mode enabled.
|
||||
Deprecated,
|
||||
/// The rule has been removed, errors will be displayed on use.
|
||||
Removed,
|
||||
/// Legacy category for unstable rules, supports backwards compatible selection.
|
||||
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
|
||||
Nursery,
|
||||
@@ -265,7 +270,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
|
||||
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
|
||||
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
|
||||
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
|
||||
(Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary),
|
||||
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
|
||||
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
|
||||
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
|
||||
@@ -306,11 +311,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Async, "102") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOsCallInAsyncFunction),
|
||||
|
||||
// flake8-trio
|
||||
(Flake8Trio, "100") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
|
||||
(Flake8Trio, "105") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioSyncCall),
|
||||
(Flake8Trio, "109") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioAsyncFunctionWithTimeout),
|
||||
(Flake8Trio, "110") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioUnneededSleep),
|
||||
(Flake8Trio, "115") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioZeroSleepCall),
|
||||
(Flake8Trio, "100") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
|
||||
(Flake8Trio, "105") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioSyncCall),
|
||||
(Flake8Trio, "109") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioAsyncFunctionWithTimeout),
|
||||
(Flake8Trio, "110") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioUnneededSleep),
|
||||
(Flake8Trio, "115") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioZeroSleepCall),
|
||||
|
||||
// flake8-builtins
|
||||
(Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing),
|
||||
@@ -351,6 +356,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bugbear, "032") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation),
|
||||
(Flake8Bugbear, "033") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateValue),
|
||||
(Flake8Bugbear, "034") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReSubPositionalArgs),
|
||||
(Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension),
|
||||
(Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept),
|
||||
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
|
||||
|
||||
@@ -417,14 +423,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Quotes, "001") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesMultilineString),
|
||||
(Flake8Quotes, "002") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesDocstring),
|
||||
(Flake8Quotes, "003") => (RuleGroup::Stable, rules::flake8_quotes::rules::AvoidableEscapedQuote),
|
||||
(Flake8Quotes, "004") => (RuleGroup::Preview, rules::flake8_quotes::rules::UnnecessaryEscapedQuote),
|
||||
(Flake8Quotes, "004") => (RuleGroup::Stable, rules::flake8_quotes::rules::UnnecessaryEscapedQuote),
|
||||
|
||||
// flake8-annotations
|
||||
(Flake8Annotations, "001") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeFunctionArgument),
|
||||
(Flake8Annotations, "002") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeArgs),
|
||||
(Flake8Annotations, "003") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeKwargs),
|
||||
(Flake8Annotations, "101") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeSelf),
|
||||
(Flake8Annotations, "102") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeCls),
|
||||
(Flake8Annotations, "101") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeSelf),
|
||||
(Flake8Annotations, "102") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeCls),
|
||||
(Flake8Annotations, "201") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeUndocumentedPublicFunction),
|
||||
(Flake8Annotations, "202") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypePrivateFunction),
|
||||
(Flake8Annotations, "204") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod),
|
||||
@@ -458,7 +464,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "109") => (RuleGroup::Stable, rules::flake8_simplify::rules::CompareWithTuple),
|
||||
(Flake8Simplify, "110") => (RuleGroup::Stable, rules::flake8_simplify::rules::ReimplementedBuiltin),
|
||||
(Flake8Simplify, "112") => (RuleGroup::Stable, rules::flake8_simplify::rules::UncapitalizedEnvironmentVariables),
|
||||
(Flake8Simplify, "113") => (RuleGroup::Preview, rules::flake8_simplify::rules::EnumerateForLoop),
|
||||
(Flake8Simplify, "113") => (RuleGroup::Stable, rules::flake8_simplify::rules::EnumerateForLoop),
|
||||
(Flake8Simplify, "114") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfWithSameArms),
|
||||
(Flake8Simplify, "115") => (RuleGroup::Stable, rules::flake8_simplify::rules::OpenFileWithContextHandler),
|
||||
(Flake8Simplify, "116") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictLookup),
|
||||
@@ -477,7 +483,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
|
||||
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
|
||||
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
|
||||
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
|
||||
(Flake8Simplify, "911") => (RuleGroup::Stable, rules::flake8_simplify::rules::ZipDictKeysAndValues),
|
||||
|
||||
// flake8-copyright
|
||||
#[allow(deprecated)]
|
||||
@@ -522,7 +528,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "038") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604Isinstance),
|
||||
(Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses),
|
||||
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
|
||||
(Pyupgrade, "041") => (RuleGroup::Preview, rules::pyupgrade::rules::TimeoutErrorAlias),
|
||||
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
|
||||
|
||||
// pydocstyle
|
||||
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
|
||||
@@ -609,8 +615,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "110") => (RuleGroup::Stable, rules::flake8_bandit::rules::TryExceptPass),
|
||||
(Flake8Bandit, "112") => (RuleGroup::Stable, rules::flake8_bandit::rules::TryExceptContinue),
|
||||
(Flake8Bandit, "113") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithoutTimeout),
|
||||
(Flake8Bandit, "201") => (RuleGroup::Preview, rules::flake8_bandit::rules::FlaskDebugTrue),
|
||||
(Flake8Bandit, "202") => (RuleGroup::Preview, rules::flake8_bandit::rules::TarfileUnsafeMembers),
|
||||
(Flake8Bandit, "201") => (RuleGroup::Stable, rules::flake8_bandit::rules::FlaskDebugTrue),
|
||||
(Flake8Bandit, "202") => (RuleGroup::Stable, rules::flake8_bandit::rules::TarfileUnsafeMembers),
|
||||
(Flake8Bandit, "301") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousPickleUsage),
|
||||
(Flake8Bandit, "302") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousMarshalUsage),
|
||||
(Flake8Bandit, "303") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousInsecureHashUsage),
|
||||
@@ -648,12 +654,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
|
||||
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
|
||||
(Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation),
|
||||
(Flake8Bandit, "502") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslInsecureVersion),
|
||||
(Flake8Bandit, "503") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithBadDefaults),
|
||||
(Flake8Bandit, "504") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithNoVersion),
|
||||
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
|
||||
(Flake8Bandit, "502") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslInsecureVersion),
|
||||
(Flake8Bandit, "503") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslWithBadDefaults),
|
||||
(Flake8Bandit, "504") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslWithNoVersion),
|
||||
(Flake8Bandit, "505") => (RuleGroup::Stable, rules::flake8_bandit::rules::WeakCryptographicKey),
|
||||
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
|
||||
(Flake8Bandit, "507") => (RuleGroup::Preview, rules::flake8_bandit::rules::SSHNoHostKeyVerification),
|
||||
(Flake8Bandit, "507") => (RuleGroup::Stable, rules::flake8_bandit::rules::SSHNoHostKeyVerification),
|
||||
(Flake8Bandit, "508") => (RuleGroup::Stable, rules::flake8_bandit::rules::SnmpInsecureVersion),
|
||||
(Flake8Bandit, "509") => (RuleGroup::Stable, rules::flake8_bandit::rules::SnmpWeakCryptography),
|
||||
(Flake8Bandit, "601") => (RuleGroup::Stable, rules::flake8_bandit::rules::ParamikoCall),
|
||||
@@ -665,10 +671,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
|
||||
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
|
||||
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
|
||||
(Flake8Bandit, "611") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoRawSql),
|
||||
(Flake8Bandit, "611") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoRawSql),
|
||||
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
|
||||
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
|
||||
(Flake8Bandit, "702") => (RuleGroup::Preview, rules::flake8_bandit::rules::MakoTemplates),
|
||||
(Flake8Bandit, "702") => (RuleGroup::Stable, rules::flake8_bandit::rules::MakoTemplates),
|
||||
|
||||
// flake8-boolean-trap
|
||||
(Flake8BooleanTrap, "001") => (RuleGroup::Stable, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument),
|
||||
@@ -699,8 +705,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Datetimez, "012") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateFromtimestamp),
|
||||
|
||||
// pygrep-hooks
|
||||
(PygrepHooks, "001") => (RuleGroup::Stable, rules::pygrep_hooks::rules::Eval),
|
||||
(PygrepHooks, "002") => (RuleGroup::Stable, rules::pygrep_hooks::rules::DeprecatedLogWarn),
|
||||
(PygrepHooks, "001") => (RuleGroup::Removed, rules::pygrep_hooks::rules::Eval),
|
||||
(PygrepHooks, "002") => (RuleGroup::Removed, rules::pygrep_hooks::rules::DeprecatedLogWarn),
|
||||
(PygrepHooks, "003") => (RuleGroup::Stable, rules::pygrep_hooks::rules::BlanketTypeIgnore),
|
||||
(PygrepHooks, "004") => (RuleGroup::Stable, rules::pygrep_hooks::rules::BlanketNOQA),
|
||||
(PygrepHooks, "005") => (RuleGroup::Stable, rules::pygrep_hooks::rules::InvalidMockAccess),
|
||||
@@ -773,7 +779,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "053") => (RuleGroup::Stable, rules::flake8_pyi::rules::StringOrBytesTooLong),
|
||||
(Flake8Pyi, "055") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnnecessaryTypeUnion),
|
||||
(Flake8Pyi, "056") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
|
||||
(Flake8Pyi, "058") => (RuleGroup::Preview, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod),
|
||||
(Flake8Pyi, "058") => (RuleGroup::Stable, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod),
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),
|
||||
@@ -835,13 +841,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
|
||||
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||
|
||||
// tryceratops
|
||||
(Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass),
|
||||
(Tryceratops, "003") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaArgs),
|
||||
(Tryceratops, "004") => (RuleGroup::Stable, rules::tryceratops::rules::TypeCheckWithoutTypeError),
|
||||
(Tryceratops, "200") => (RuleGroup::Stable, rules::tryceratops::rules::ReraiseNoCause),
|
||||
(Tryceratops, "200") => (RuleGroup::Removed, rules::tryceratops::rules::ReraiseNoCause),
|
||||
(Tryceratops, "201") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseRaise),
|
||||
(Tryceratops, "300") => (RuleGroup::Stable, rules::tryceratops::rules::TryConsiderElse),
|
||||
(Tryceratops, "301") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseWithinTry),
|
||||
@@ -904,7 +910,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Numpy, "001") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedTypeAlias),
|
||||
(Numpy, "002") => (RuleGroup::Stable, rules::numpy::rules::NumpyLegacyRandom),
|
||||
(Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction),
|
||||
(Numpy, "201") => (RuleGroup::Preview, rules::numpy::rules::Numpy2Deprecation),
|
||||
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),
|
||||
|
||||
// ruff
|
||||
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
|
||||
@@ -916,24 +922,52 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "008") => (RuleGroup::Stable, rules::ruff::rules::MutableDataclassDefault),
|
||||
(Ruff, "009") => (RuleGroup::Stable, rules::ruff::rules::FunctionCallInDataclassDefaultArgument),
|
||||
(Ruff, "010") => (RuleGroup::Stable, rules::ruff::rules::ExplicitFStringTypeConversion),
|
||||
(Ruff, "011") => (RuleGroup::Stable, rules::ruff::rules::StaticKeyDictComprehension),
|
||||
(Ruff, "011") => (RuleGroup::Removed, rules::ruff::rules::RuffStaticKeyDictComprehension),
|
||||
(Ruff, "012") => (RuleGroup::Stable, rules::ruff::rules::MutableClassDefault),
|
||||
(Ruff, "013") => (RuleGroup::Stable, rules::ruff::rules::ImplicitOptional),
|
||||
(Ruff, "015") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Stable, rules::ruff::rules::InvalidIndexType),
|
||||
#[allow(deprecated)]
|
||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
|
||||
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
|
||||
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
|
||||
(Ruff, "017") => (RuleGroup::Stable, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "018") => (RuleGroup::Stable, rules::ruff::rules::AssignmentInAssert),
|
||||
(Ruff, "019") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryKeyCheck),
|
||||
(Ruff, "020") => (RuleGroup::Stable, rules::ruff::rules::NeverUnion),
|
||||
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
|
||||
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
|
||||
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
|
||||
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
|
||||
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
|
||||
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
|
||||
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "901") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleSafeFix),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "902") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleUnsafeFix),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "903") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleDisplayOnlyFix),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
#[allow(deprecated)]
|
||||
(Ruff, "912") => (RuleGroup::Nursery, rules::ruff::rules::NurseryTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "920") => (RuleGroup::Deprecated, rules::ruff::rules::DeprecatedTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "921") => (RuleGroup::Deprecated, rules::ruff::rules::AnotherDeprecatedTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "930") => (RuleGroup::Removed, rules::ruff::rules::RemovedTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "931") => (RuleGroup::Removed, rules::ruff::rules::AnotherRemovedTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "940") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "950") => (RuleGroup::Stable, rules::ruff::rules::RedirectedToTestRule),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "960") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromPrefixTestRule),
|
||||
|
||||
|
||||
// flake8-django
|
||||
(Flake8Django, "001") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoNullableModelStringField),
|
||||
@@ -1001,13 +1035,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
|
||||
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
|
||||
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
|
||||
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
|
||||
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
|
||||
|
||||
// flake8-logging
|
||||
(Flake8Logging, "001") => (RuleGroup::Preview, rules::flake8_logging::rules::DirectLoggerInstantiation),
|
||||
(Flake8Logging, "002") => (RuleGroup::Preview, rules::flake8_logging::rules::InvalidGetLoggerArgument),
|
||||
(Flake8Logging, "007") => (RuleGroup::Preview, rules::flake8_logging::rules::ExceptionWithoutExcInfo),
|
||||
(Flake8Logging, "009") => (RuleGroup::Preview, rules::flake8_logging::rules::UndocumentedWarn),
|
||||
(Flake8Logging, "001") => (RuleGroup::Stable, rules::flake8_logging::rules::DirectLoggerInstantiation),
|
||||
(Flake8Logging, "002") => (RuleGroup::Stable, rules::flake8_logging::rules::InvalidGetLoggerArgument),
|
||||
(Flake8Logging, "007") => (RuleGroup::Stable, rules::flake8_logging::rules::ExceptionWithoutExcInfo),
|
||||
(Flake8Logging, "009") => (RuleGroup::Stable, rules::flake8_logging::rules::UndocumentedWarn),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
|
||||
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::textwrap::dedent_to;
|
||||
use ruff_python_trivia::{
|
||||
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
|
||||
SimpleTokenizer,
|
||||
@@ -15,7 +16,9 @@ use ruff_python_trivia::{
|
||||
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::cst::matchers::{match_function_def, match_indented_block, match_statement};
|
||||
use crate::fix::codemods;
|
||||
use crate::fix::codemods::CodegenStylist;
|
||||
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
|
||||
|
||||
/// Return the `Fix` to use when deleting a `Stmt`.
|
||||
@@ -166,6 +169,50 @@ pub(crate) fn add_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely adjust the indentation of the indented block at [`TextRange`].
|
||||
///
|
||||
/// The [`TextRange`] is assumed to represent an entire indented block, including the leading
|
||||
/// indentation of that block. For example, to dedent the body here:
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// print("Hello, world!")
|
||||
/// ```
|
||||
///
|
||||
/// The range would be the entirety of ` print("Hello, world!")`.
|
||||
pub(crate) fn adjust_indentation(
|
||||
range: TextRange,
|
||||
indentation: &str,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
) -> Result<String> {
|
||||
// If the range includes a multi-line string, use LibCST to ensure that we don't adjust the
|
||||
// whitespace _within_ the string.
|
||||
if indexer.multiline_ranges().intersects(range) || indexer.fstring_ranges().intersects(range) {
|
||||
let contents = locator.slice(range);
|
||||
|
||||
let module_text = format!("def f():{}{contents}", stylist.line_ending().as_str());
|
||||
|
||||
let mut tree = match_statement(&module_text)?;
|
||||
|
||||
let embedding = match_function_def(&mut tree)?;
|
||||
|
||||
let indented_block = match_indented_block(&mut embedding.body)?;
|
||||
indented_block.indent = Some(indentation);
|
||||
|
||||
let module_text = indented_block.codegen_stylist(stylist);
|
||||
let module_text = module_text
|
||||
.strip_prefix(stylist.line_ending().as_str())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Ok(module_text)
|
||||
} else {
|
||||
// Otherwise, we can do a simple adjustment ourselves.
|
||||
let contents = locator.slice(range);
|
||||
Ok(dedent_to(contents, indentation))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a vector contains only one, specific element.
|
||||
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
|
||||
vec.len() == 1 && vec[0] == *value
|
||||
|
||||
@@ -33,6 +33,8 @@ use crate::message::Message;
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rules::pycodestyle;
|
||||
#[cfg(feature = "test-rules")]
|
||||
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
|
||||
use crate::settings::types::UnsafeFixes;
|
||||
use crate::settings::{flags, LinterSettings};
|
||||
use crate::source_kind::SourceKind;
|
||||
@@ -214,6 +216,53 @@ pub fn check_path(
|
||||
));
|
||||
}
|
||||
|
||||
// Raise violations for internal test rules
|
||||
#[cfg(feature = "test-rules")]
|
||||
{
|
||||
for test_rule in TEST_RULES {
|
||||
if !settings.rules.enabled(*test_rule) {
|
||||
continue;
|
||||
}
|
||||
let diagnostic = match test_rule {
|
||||
Rule::StableTestRule => test_rules::StableTestRule::diagnostic(locator, indexer),
|
||||
Rule::StableTestRuleSafeFix => {
|
||||
test_rules::StableTestRuleSafeFix::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::StableTestRuleUnsafeFix => {
|
||||
test_rules::StableTestRuleUnsafeFix::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::StableTestRuleDisplayOnlyFix => {
|
||||
test_rules::StableTestRuleDisplayOnlyFix::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::NurseryTestRule => test_rules::NurseryTestRule::diagnostic(locator, indexer),
|
||||
Rule::PreviewTestRule => test_rules::PreviewTestRule::diagnostic(locator, indexer),
|
||||
Rule::DeprecatedTestRule => {
|
||||
test_rules::DeprecatedTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::AnotherDeprecatedTestRule => {
|
||||
test_rules::AnotherDeprecatedTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::RemovedTestRule => test_rules::RemovedTestRule::diagnostic(locator, indexer),
|
||||
Rule::AnotherRemovedTestRule => {
|
||||
test_rules::AnotherRemovedTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::RedirectedToTestRule => {
|
||||
test_rules::RedirectedToTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::RedirectedFromTestRule => {
|
||||
test_rules::RedirectedFromTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
Rule::RedirectedFromPrefixTestRule => {
|
||||
test_rules::RedirectedFromPrefixTestRule::diagnostic(locator, indexer)
|
||||
}
|
||||
_ => unreachable!("All test rules must have an implementation"),
|
||||
};
|
||||
if let Some(diagnostic) = diagnostic {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore diagnostics based on per-file-ignores.
|
||||
let per_file_ignores = if !diagnostics.is_empty() && !settings.per_file_ignores.is_empty() {
|
||||
fs::ignores_from_path(path, &settings.per_file_ignores)
|
||||
@@ -539,7 +588,7 @@ pub fn lint_fix<'a>(
|
||||
// Increment the iteration count.
|
||||
iterations += 1;
|
||||
|
||||
// Re-run the linter pass (by avoiding the break).
|
||||
// Re-run the linter pass (by avoiding the return).
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use fern;
|
||||
use log::Level;
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_python_parser::{ParseError, ParseErrorType};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode, SourceLocation};
|
||||
|
||||
@@ -15,7 +16,7 @@ use crate::fs;
|
||||
use crate::source_kind::SourceKind;
|
||||
use ruff_notebook::Notebook;
|
||||
|
||||
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
pub static IDENTIFIERS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
|
||||
/// Warn a user once, with uniqueness determined by the given ID.
|
||||
#[macro_export]
|
||||
@@ -24,7 +25,7 @@ macro_rules! warn_user_once_by_id {
|
||||
use colored::Colorize;
|
||||
use log::warn;
|
||||
|
||||
if let Ok(mut states) = $crate::logging::WARNINGS.lock() {
|
||||
if let Ok(mut states) = $crate::logging::IDENTIFIERS.lock() {
|
||||
if !states.contains(&$id) {
|
||||
let message = format!("{}", format_args!($($arg)*));
|
||||
warn!("{}", message.bold());
|
||||
@@ -34,6 +35,26 @@ macro_rules! warn_user_once_by_id {
|
||||
};
|
||||
}
|
||||
|
||||
pub static MESSAGES: Lazy<Mutex<FxHashSet<String>>> = Lazy::new(Mutex::default);
|
||||
|
||||
/// Warn a user once, if warnings are enabled, with uniqueness determined by the content of the
|
||||
/// message.
|
||||
#[macro_export]
|
||||
macro_rules! warn_user_once_by_message {
|
||||
($($arg:tt)*) => {
|
||||
use colored::Colorize;
|
||||
use log::warn;
|
||||
|
||||
if let Ok(mut states) = $crate::logging::MESSAGES.lock() {
|
||||
let message = format!("{}", format_args!($($arg)*));
|
||||
if !states.contains(&message) {
|
||||
warn!("{}", message.bold());
|
||||
states.insert(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Warn a user once, with uniqueness determined by the calling location itself.
|
||||
#[macro_export]
|
||||
macro_rules! warn_user_once {
|
||||
|
||||
@@ -98,5 +98,16 @@ static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
|
||||
("T002", "FIX002"),
|
||||
("T003", "FIX003"),
|
||||
("T004", "FIX004"),
|
||||
("RUF011", "B035"),
|
||||
("TCH006", "TCH010"),
|
||||
("TRY200", "B904"),
|
||||
("PGH001", "S307"),
|
||||
("PGH002", "G010"),
|
||||
// Test redirect by exact code
|
||||
#[cfg(feature = "test-rules")]
|
||||
("RUF940", "RUF950"),
|
||||
// Test redirect by prefix
|
||||
#[cfg(feature = "test-rules")]
|
||||
("RUF96", "RUF95"),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -44,10 +44,25 @@ impl From<Linter> for RuleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for RuleSelector {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// TODO(zanieb): We ought to put "ALL" and "Linter" selectors
|
||||
// above those that are rule specific but it's not critical for now
|
||||
self.prefix_and_code().cmp(&other.prefix_and_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RuleSelector {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RuleSelector {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// **Changes should be reflected in `parse_no_redirect` as well**
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
#[allow(deprecated)]
|
||||
@@ -67,7 +82,6 @@ impl FromStr for RuleSelector {
|
||||
return Ok(Self::Linter(linter));
|
||||
}
|
||||
|
||||
// Does the selector select a single rule?
|
||||
let prefix = RuleCodePrefix::parse(&linter, code)
|
||||
.map_err(|_| ParseError::Unknown(s.to_string()))?;
|
||||
|
||||
@@ -172,7 +186,7 @@ impl Visitor<'_> for SelectorVisitor {
|
||||
}
|
||||
|
||||
impl RuleSelector {
|
||||
/// Return all matching rules, regardless of whether they're in preview.
|
||||
/// Return all matching rules, regardless of rule group filters like preview and deprecated.
|
||||
pub fn all_rules(&self) -> impl Iterator<Item = Rule> + '_ {
|
||||
match self {
|
||||
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
|
||||
@@ -198,21 +212,30 @@ impl RuleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns rules matching the selector, taking into account preview options enabled.
|
||||
/// Returns rules matching the selector, taking into account rule groups like preview and deprecated.
|
||||
pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator<Item = Rule> + 'a {
|
||||
let preview_enabled = preview.mode.is_enabled();
|
||||
let preview_require_explicit = preview.require_explicit;
|
||||
#[allow(deprecated)]
|
||||
self.all_rules().filter(move |rule| {
|
||||
// Always include rules that are not in preview or the nursery
|
||||
!(rule.is_preview() || rule.is_nursery())
|
||||
// Always include stable rules
|
||||
rule.is_stable()
|
||||
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|
||||
|| ((matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
|
||||
|| ((self.is_exact() || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
|
||||
// Enabling preview includes all preview or nursery rules unless explicit selection
|
||||
// is turned on
|
||||
|| (preview_enabled && (matches!(self, RuleSelector::Rule { .. }) || !preview_require_explicit))
|
||||
|| ((rule.is_preview() || rule.is_nursery()) && preview_enabled && (self.is_exact() || !preview_require_explicit))
|
||||
// Deprecated rules are excluded in preview mode unless explicitly selected
|
||||
|| (rule.is_deprecated() && (!preview_enabled || self.is_exact()))
|
||||
// Removed rules are included if explicitly selected but will error downstream
|
||||
|| (rule.is_removed() && self.is_exact())
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if this selector is exact i.e. selects a single rule by code
|
||||
pub fn is_exact(&self) -> bool {
|
||||
matches!(self, Self::Rule { .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RuleSelectorIter {
|
||||
@@ -267,7 +290,6 @@ mod schema {
|
||||
[
|
||||
// Include the non-standard "ALL" and "NURSERY" selectors.
|
||||
"ALL".to_string(),
|
||||
"NURSERY".to_string(),
|
||||
// Include the legacy "C" and "T" selectors.
|
||||
"C".to_string(),
|
||||
"T".to_string(),
|
||||
@@ -289,6 +311,16 @@ mod schema {
|
||||
(!prefix.is_empty()).then(|| prefix.to_string())
|
||||
})),
|
||||
)
|
||||
.filter(|p| {
|
||||
// Exclude any prefixes where all of the rules are removed
|
||||
if let Ok(Self::Rule { prefix, .. } | Self::Prefix { prefix, .. }) =
|
||||
RuleSelector::parse_no_redirect(p)
|
||||
{
|
||||
!prefix.rules().all(|rule| rule.is_removed())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.sorted()
|
||||
.map(Value::String)
|
||||
.collect(),
|
||||
@@ -321,6 +353,41 @@ impl RuleSelector {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse [`RuleSelector`] from a string; but do not follow redirects.
|
||||
pub fn parse_no_redirect(s: &str) -> Result<Self, ParseError> {
|
||||
// **Changes should be reflected in `from_str` as well**
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
#[allow(deprecated)]
|
||||
"NURSERY" => Ok(Self::Nursery),
|
||||
"C" => Ok(Self::C),
|
||||
"T" => Ok(Self::T),
|
||||
_ => {
|
||||
let (linter, code) =
|
||||
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
|
||||
|
||||
if code.is_empty() {
|
||||
return Ok(Self::Linter(linter));
|
||||
}
|
||||
|
||||
let prefix = RuleCodePrefix::parse(&linter, code)
|
||||
.map_err(|_| ParseError::Unknown(s.to_string()))?;
|
||||
|
||||
if is_single_rule_selector(&prefix) {
|
||||
Ok(Self::Rule {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
})
|
||||
} else {
|
||||
Ok(Self::Prefix {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/// See: [eradicate.py](https://github.com/myint/eradicate/blob/98f199940979c94447a461d50d27862b118b282d/eradicate.py)
|
||||
use aho_corasick::AhoCorasick;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Regex, RegexSet};
|
||||
|
||||
use ruff_python_parser::parse_suite;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
static CODE_INDICATORS: Lazy<AhoCorasick> = Lazy::new(|| {
|
||||
AhoCorasick::new([
|
||||
"(", ")", "[", "]", "{", "}", ":", "=", "%", "print", "return", "break", "continue",
|
||||
"import",
|
||||
"(", ")", "[", "]", "{", "}", ":", "=", "%", "return", "break", "continue", "import",
|
||||
])
|
||||
.unwrap()
|
||||
});
|
||||
@@ -44,6 +46,14 @@ pub(crate) fn comment_contains_code(line: &str, task_tags: &[String]) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path: if the comment contains consecutive identifiers, we know it won't parse.
|
||||
let tokenizer = SimpleTokenizer::starts_at(TextSize::default(), line).skip_trivia();
|
||||
if tokenizer.tuple_windows().any(|(first, second)| {
|
||||
first.kind == SimpleTokenKind::Name && second.kind == SimpleTokenKind::Name
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore task tag comments (e.g., "# TODO(tom): Refactor").
|
||||
if line
|
||||
.split(&[' ', ':', '('])
|
||||
@@ -123,9 +133,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn comment_contains_code_with_print() {
|
||||
assert!(comment_contains_code("#print", &[]));
|
||||
assert!(comment_contains_code("#print(1)", &[]));
|
||||
|
||||
assert!(!comment_contains_code("#print", &[]));
|
||||
assert!(!comment_contains_code("#print 1", &[]));
|
||||
assert!(!comment_contains_code("#to print", &[]));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use super::super::detection::comment_contains_code;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `task-tags`
|
||||
/// - `lint.task-tags`
|
||||
///
|
||||
/// [#4845]: https://github.com/astral-sh/ruff/issues/4845
|
||||
#[violation]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -47,6 +47,10 @@ impl Violation for SixPY3 {
|
||||
|
||||
/// YTT202
|
||||
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::SIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
@@ -111,6 +111,10 @@ impl Violation for MissingTypeKwargs {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Deprecation
|
||||
/// This rule is commonly disabled because type checkers can infer this type without annotation.
|
||||
/// It will be removed in a future release.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks that instance method `self` arguments have type annotations.
|
||||
///
|
||||
@@ -148,6 +152,10 @@ impl Violation for MissingTypeSelf {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Deprecation
|
||||
/// This rule is commonly disabled because type checkers can infer this type without annotation.
|
||||
/// It will be removed in a future release.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks that class method `cls` arguments have type annotations.
|
||||
///
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_python_ast::ExprCall;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -42,17 +43,19 @@ impl Violation for BlockingOsCallInAsyncFunction {
|
||||
|
||||
/// ASYNC102
|
||||
pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if checker.semantic().in_async_context() {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.as_ref()
|
||||
.is_some_and(is_unsafe_os_method)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingOsCallInAsyncFunction,
|
||||
call.func.range(),
|
||||
));
|
||||
if checker.semantic().seen_module(Modules::OS) {
|
||||
if checker.semantic().in_async_context() {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.as_ref()
|
||||
.is_some_and(is_unsafe_os_method)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingOsCallInAsyncFunction,
|
||||
call.func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -60,6 +60,10 @@ enum Reason {
|
||||
|
||||
/// S103
|
||||
pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::OS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -35,6 +36,10 @@ impl Violation for DjangoRawSql {
|
||||
|
||||
/// S611
|
||||
pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -35,6 +36,10 @@ impl Violation for LoggingConfigInsecureListen {
|
||||
|
||||
/// S612
|
||||
pub(crate) fn logging_config_insecure_listen(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::LOGGING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
@@ -48,6 +49,10 @@ impl Violation for TarfileUnsafeMembers {
|
||||
|
||||
/// S202
|
||||
pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TARFILE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !call
|
||||
.func
|
||||
.as_attribute_expr()
|
||||
@@ -65,10 +70,6 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker.semantic().seen(&["tarfile"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(TarfileUnsafeMembers, call.func.range()));
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::checkers::ast::Checker;
|
||||
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
|
||||
///
|
||||
/// dsa.generate_private_key(key_size=512)
|
||||
/// ec.generate_private_key(curve=ec.SECT163K1)
|
||||
/// ec.generate_private_key(curve=ec.SECT163K1())
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
@@ -27,7 +27,7 @@ use crate::checkers::ast::Checker;
|
||||
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
|
||||
///
|
||||
/// dsa.generate_private_key(key_size=4096)
|
||||
/// ec.generate_private_key(curve=ec.SECP384R1)
|
||||
/// ec.generate_private_key(curve=ec.SECP384R1())
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -191,6 +191,11 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel)
|
||||
}
|
||||
// Ex) `typing.Union[bool, int]`
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
// If the typing modules were never imported, we'll never match below.
|
||||
if !semantic.seen_typing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let call_path = semantic.resolve_call_path(value);
|
||||
if call_path
|
||||
.as_ref()
|
||||
|
||||
@@ -12,7 +12,7 @@ mod tests {
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
|
||||
@@ -34,7 +34,6 @@ mod tests {
|
||||
#[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))]
|
||||
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))]
|
||||
#[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))]
|
||||
@@ -42,6 +41,7 @@ mod tests {
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_5.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_6.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
|
||||
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
|
||||
#[test_case(Rule::RaiseLiteral, Path::new("B016.py"))]
|
||||
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))]
|
||||
@@ -50,16 +50,17 @@ mod tests {
|
||||
#[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))]
|
||||
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))]
|
||||
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))]
|
||||
#[test_case(Rule::StaticKeyDictComprehension, Path::new("B035.py"))]
|
||||
#[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))]
|
||||
#[test_case(Rule::UnaryPrefixIncrementDecrement, Path::new("B002.py"))]
|
||||
#[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))]
|
||||
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))]
|
||||
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))]
|
||||
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
|
||||
#[test_case(Rule::UselessComparison, Path::new("B015.ipynb"))]
|
||||
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
|
||||
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
|
||||
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
|
||||
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
|
||||
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
@@ -70,24 +71,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::DuplicateValue, Path::new("B033.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_bugbear").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_without_explicit_strict() -> Result<()> {
|
||||
let snapshot = "B905.py";
|
||||
|
||||
@@ -61,11 +61,9 @@ pub(crate) fn duplicate_value(checker: &mut Checker, set: &ast::ExprSet) {
|
||||
elt.range(),
|
||||
);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_member(set, index, checker.locator().contents()).map(Fix::safe_edit)
|
||||
});
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_member(set, index, checker.locator().contents()).map(Fix::safe_edit)
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ use crate::checkers::ast::Checker;
|
||||
/// calls to the function, which can lead to unexpected behaviour.
|
||||
///
|
||||
/// Calls can be marked as an exception to this rule with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option as well.
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option as well.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -61,7 +61,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-bugbear.extend-immutable-calls`
|
||||
/// - `lint.flake8-bugbear.extend-immutable-calls`
|
||||
#[violation]
|
||||
pub struct FunctionCallInDefaultArgument {
|
||||
name: Option<String>,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) use redundant_tuple_in_exception_handler::*;
|
||||
pub(crate) use reuse_of_groupby_generator::*;
|
||||
pub(crate) use setattr_with_constant::*;
|
||||
pub(crate) use star_arg_unpacking_after_keyword_arg::*;
|
||||
pub(crate) use static_key_dict_comprehension::*;
|
||||
pub(crate) use strip_with_multi_characters::*;
|
||||
pub(crate) use unary_prefix_increment_decrement::*;
|
||||
pub(crate) use unintentional_type_annotation::*;
|
||||
@@ -56,6 +57,7 @@ mod redundant_tuple_in_exception_handler;
|
||||
mod reuse_of_groupby_generator;
|
||||
mod setattr_with_constant;
|
||||
mod star_arg_unpacking_after_keyword_arg;
|
||||
mod static_key_dict_comprehension;
|
||||
mod strip_with_multi_characters;
|
||||
mod unary_prefix_increment_decrement;
|
||||
mod unintentional_type_annotation;
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Mutable argument defaults can be used intentionally to cache computation
|
||||
@@ -61,7 +61,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-bugbear.extend-immutable-calls`
|
||||
/// - `lint.flake8-bugbear.extend-immutable-calls`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
|
||||
@@ -83,7 +83,6 @@ impl Violation for MutableArgumentDefault {
|
||||
|
||||
/// B006
|
||||
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
|
||||
// Scan in reverse order to right-align zip().
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default,
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_python_ast::{self as ast};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -56,6 +57,10 @@ impl Violation for ReSubPositionalArgs {
|
||||
|
||||
/// B034
|
||||
pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::RE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(method) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::StoredNameFinder;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for dictionary comprehensions that use a static key, like a string
|
||||
/// literal or a variable defined outside the comprehension.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using a static key (like a string literal) in a dictionary comprehension
|
||||
/// is usually a mistake, as it will result in a dictionary with only one key,
|
||||
/// despite the comprehension iterating over multiple values.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// data = ["some", "Data"]
|
||||
/// {"key": value.upper() for value in data}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// data = ["some", "Data"]
|
||||
/// {value: value.upper() for value in data}
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct StaticKeyDictComprehension {
|
||||
key: SourceCodeSnippet,
|
||||
}
|
||||
|
||||
impl Violation for StaticKeyDictComprehension {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let StaticKeyDictComprehension { key } = self;
|
||||
if let Some(key) = key.full_display() {
|
||||
format!("Dictionary comprehension uses static key: `{key}`")
|
||||
} else {
|
||||
format!("Dictionary comprehension uses static key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF011
|
||||
pub(crate) fn static_key_dict_comprehension(checker: &mut Checker, dict_comp: &ast::ExprDictComp) {
|
||||
// Collect the bound names in the comprehension's generators.
|
||||
let names = {
|
||||
let mut visitor = StoredNameFinder::default();
|
||||
for generator in &dict_comp.generators {
|
||||
visitor.visit_comprehension(generator);
|
||||
}
|
||||
visitor.names
|
||||
};
|
||||
|
||||
if is_constant(&dict_comp.key, &names) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StaticKeyDictComprehension {
|
||||
key: SourceCodeSnippet::from_str(checker.locator().slice(dict_comp.key.as_ref())),
|
||||
},
|
||||
dict_comp.key.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression is a constant in the context of the dictionary
|
||||
/// comprehension.
|
||||
fn is_constant(key: &Expr, names: &FxHashMap<&str, &ast::ExprName>) -> bool {
|
||||
match key {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(|elt| is_constant(elt, names)),
|
||||
Expr::Name(ast::ExprName { id, .. }) => !names.contains_key(id.as_str()),
|
||||
Expr::Attribute(ast::ExprAttribute { value, .. }) => is_constant(value, names),
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
is_constant(value, names) && is_constant(slice, names)
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => {
|
||||
is_constant(left, names) && is_constant(right, names)
|
||||
}
|
||||
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
|
||||
values.iter().all(|value| is_constant(value, names))
|
||||
}
|
||||
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_constant(operand, names),
|
||||
expr if expr.is_literal_expr() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B033.py:4:35: B033 Sets should not contain duplicate item `"value1"`
|
||||
B033.py:4:35: B033 [*] Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
2 | # Errors.
|
||||
3 | ###
|
||||
@@ -12,7 +12,17 @@ B033.py:4:35: B033 Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:5:21: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
1 1 | ###
|
||||
2 2 | # Errors.
|
||||
3 3 | ###
|
||||
4 |-incorrect_set = {"value1", 23, 5, "value1"}
|
||||
4 |+incorrect_set = {"value1", 23, 5}
|
||||
5 5 | incorrect_set = {1, 1, 2}
|
||||
6 6 | incorrect_set_multiline = {
|
||||
7 7 | "value1",
|
||||
|
||||
B033.py:5:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
3 | ###
|
||||
4 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
@@ -23,7 +33,17 @@ B033.py:5:21: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:10:5: B033 Sets should not contain duplicate item `"value1"`
|
||||
ℹ Safe fix
|
||||
2 2 | # Errors.
|
||||
3 3 | ###
|
||||
4 4 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
5 |-incorrect_set = {1, 1, 2}
|
||||
5 |+incorrect_set = {1, 2}
|
||||
6 6 | incorrect_set_multiline = {
|
||||
7 7 | "value1",
|
||||
8 8 | 23,
|
||||
|
||||
B033.py:10:5: B033 [*] Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
8 | 23,
|
||||
9 | 5,
|
||||
@@ -34,7 +54,16 @@ B033.py:10:5: B033 Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:13:21: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
7 7 | "value1",
|
||||
8 8 | 23,
|
||||
9 9 | 5,
|
||||
10 |- "value1",
|
||||
11 10 | # B033
|
||||
12 11 | }
|
||||
13 12 | incorrect_set = {1, 1}
|
||||
|
||||
B033.py:13:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
11 | # B033
|
||||
12 | }
|
||||
@@ -45,7 +74,17 @@ B033.py:13:21: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:14:21: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
10 10 | "value1",
|
||||
11 11 | # B033
|
||||
12 12 | }
|
||||
13 |-incorrect_set = {1, 1}
|
||||
13 |+incorrect_set = {1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
|
||||
B033.py:14:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
12 | }
|
||||
13 | incorrect_set = {1, 1}
|
||||
@@ -56,7 +95,17 @@ B033.py:14:21: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:15:24: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
11 11 | # B033
|
||||
12 12 | }
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 |-incorrect_set = {1, 1,}
|
||||
14 |+incorrect_set = {1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
17 17 | incorrect_set = {
|
||||
|
||||
B033.py:15:24: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
13 | incorrect_set = {1, 1}
|
||||
14 | incorrect_set = {1, 1,}
|
||||
@@ -67,7 +116,17 @@ B033.py:15:24: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:16:24: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
12 12 | }
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 |-incorrect_set = {0, 1, 1,}
|
||||
15 |+incorrect_set = {0, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
|
||||
B033.py:16:24: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
14 | incorrect_set = {1, 1,}
|
||||
15 | incorrect_set = {0, 1, 1,}
|
||||
@@ -78,7 +137,17 @@ B033.py:16:24: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
B033.py:20:5: B033 Sets should not contain duplicate item `1`
|
||||
ℹ Safe fix
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 |-incorrect_set = {0, 1, 1}
|
||||
16 |+incorrect_set = {0, 1}
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
19 19 | 1,
|
||||
|
||||
B033.py:20:5: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
18 | 0,
|
||||
19 | 1,
|
||||
@@ -88,4 +157,13 @@ B033.py:20:5: B033 Sets should not contain duplicate item `1`
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
19 19 | 1,
|
||||
20 |- 1,
|
||||
21 20 | }
|
||||
22 21 |
|
||||
23 22 | ###
|
||||
|
||||
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
RUF011.py:17:2: RUF011 Dictionary comprehension uses static key: `"key"`
|
||||
B035.py:17:2: B035 Dictionary comprehension uses static key: `"key"`
|
||||
|
|
||||
16 | # Errors
|
||||
17 | {"key": value.upper() for value in data}
|
||||
| ^^^^^ RUF011
|
||||
| ^^^^^ B035
|
||||
18 | {True: value.upper() for value in data}
|
||||
19 | {0: value.upper() for value in data}
|
||||
|
|
||||
|
||||
RUF011.py:18:2: RUF011 Dictionary comprehension uses static key: `True`
|
||||
B035.py:18:2: B035 Dictionary comprehension uses static key: `True`
|
||||
|
|
||||
16 | # Errors
|
||||
17 | {"key": value.upper() for value in data}
|
||||
18 | {True: value.upper() for value in data}
|
||||
| ^^^^ RUF011
|
||||
| ^^^^ B035
|
||||
19 | {0: value.upper() for value in data}
|
||||
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
|
|
||||
|
||||
RUF011.py:19:2: RUF011 Dictionary comprehension uses static key: `0`
|
||||
B035.py:19:2: B035 Dictionary comprehension uses static key: `0`
|
||||
|
|
||||
17 | {"key": value.upper() for value in data}
|
||||
18 | {True: value.upper() for value in data}
|
||||
19 | {0: value.upper() for value in data}
|
||||
| ^ RUF011
|
||||
| ^ B035
|
||||
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
21 | {constant: value.upper() for value in data}
|
||||
|
|
||||
|
||||
RUF011.py:20:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
|
||||
B035.py:20:2: B035 Dictionary comprehension uses static key: `(1, "a")`
|
||||
|
|
||||
18 | {True: value.upper() for value in data}
|
||||
19 | {0: value.upper() for value in data}
|
||||
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
| ^^^^^^^^ RUF011
|
||||
| ^^^^^^^^ B035
|
||||
21 | {constant: value.upper() for value in data}
|
||||
22 | {constant + constant: value.upper() for value in data}
|
||||
|
|
||||
|
||||
RUF011.py:21:2: RUF011 Dictionary comprehension uses static key: `constant`
|
||||
B035.py:21:2: B035 Dictionary comprehension uses static key: `constant`
|
||||
|
|
||||
19 | {0: value.upper() for value in data}
|
||||
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
21 | {constant: value.upper() for value in data}
|
||||
| ^^^^^^^^ RUF011
|
||||
| ^^^^^^^^ B035
|
||||
22 | {constant + constant: value.upper() for value in data}
|
||||
23 | {constant.attribute: value.upper() for value in data}
|
||||
|
|
||||
|
||||
RUF011.py:22:2: RUF011 Dictionary comprehension uses static key: `constant + constant`
|
||||
B035.py:22:2: B035 Dictionary comprehension uses static key: `constant + constant`
|
||||
|
|
||||
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||
21 | {constant: value.upper() for value in data}
|
||||
22 | {constant + constant: value.upper() for value in data}
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF011
|
||||
| ^^^^^^^^^^^^^^^^^^^ B035
|
||||
23 | {constant.attribute: value.upper() for value in data}
|
||||
24 | {constant[0]: value.upper() for value in data}
|
||||
|
|
||||
|
||||
RUF011.py:23:2: RUF011 Dictionary comprehension uses static key: `constant.attribute`
|
||||
B035.py:23:2: B035 Dictionary comprehension uses static key: `constant.attribute`
|
||||
|
|
||||
21 | {constant: value.upper() for value in data}
|
||||
22 | {constant + constant: value.upper() for value in data}
|
||||
23 | {constant.attribute: value.upper() for value in data}
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF011
|
||||
| ^^^^^^^^^^^^^^^^^^ B035
|
||||
24 | {constant[0]: value.upper() for value in data}
|
||||
25 | {tokens: token for token in tokens}
|
||||
|
|
||||
|
||||
RUF011.py:24:2: RUF011 Dictionary comprehension uses static key: `constant[0]`
|
||||
B035.py:24:2: B035 Dictionary comprehension uses static key: `constant[0]`
|
||||
|
|
||||
22 | {constant + constant: value.upper() for value in data}
|
||||
23 | {constant.attribute: value.upper() for value in data}
|
||||
24 | {constant[0]: value.upper() for value in data}
|
||||
| ^^^^^^^^^^^ RUF011
|
||||
| ^^^^^^^^^^^ B035
|
||||
25 | {tokens: token for token in tokens}
|
||||
|
|
||||
|
||||
RUF011.py:25:2: RUF011 Dictionary comprehension uses static key: `tokens`
|
||||
B035.py:25:2: B035 Dictionary comprehension uses static key: `tokens`
|
||||
|
|
||||
23 | {constant.attribute: value.upper() for value in data}
|
||||
24 | {constant[0]: value.upper() for value in data}
|
||||
25 | {tokens: token for token in tokens}
|
||||
| ^^^^^^ RUF011
|
||||
| ^^^^^^ B035
|
||||
|
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B033.py:4:35: B033 [*] Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
2 | # Errors.
|
||||
3 | ###
|
||||
4 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
| ^^^^^^^^ B033
|
||||
5 | incorrect_set = {1, 1, 2}
|
||||
6 | incorrect_set_multiline = {
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | ###
|
||||
2 2 | # Errors.
|
||||
3 3 | ###
|
||||
4 |-incorrect_set = {"value1", 23, 5, "value1"}
|
||||
4 |+incorrect_set = {"value1", 23, 5}
|
||||
5 5 | incorrect_set = {1, 1, 2}
|
||||
6 6 | incorrect_set_multiline = {
|
||||
7 7 | "value1",
|
||||
|
||||
B033.py:5:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
3 | ###
|
||||
4 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
5 | incorrect_set = {1, 1, 2}
|
||||
| ^ B033
|
||||
6 | incorrect_set_multiline = {
|
||||
7 | "value1",
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | # Errors.
|
||||
3 3 | ###
|
||||
4 4 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
5 |-incorrect_set = {1, 1, 2}
|
||||
5 |+incorrect_set = {1, 2}
|
||||
6 6 | incorrect_set_multiline = {
|
||||
7 7 | "value1",
|
||||
8 8 | 23,
|
||||
|
||||
B033.py:10:5: B033 [*] Sets should not contain duplicate item `"value1"`
|
||||
|
|
||||
8 | 23,
|
||||
9 | 5,
|
||||
10 | "value1",
|
||||
| ^^^^^^^^ B033
|
||||
11 | # B033
|
||||
12 | }
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | "value1",
|
||||
8 8 | 23,
|
||||
9 9 | 5,
|
||||
10 |- "value1",
|
||||
11 10 | # B033
|
||||
12 11 | }
|
||||
13 12 | incorrect_set = {1, 1}
|
||||
|
||||
B033.py:13:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
11 | # B033
|
||||
12 | }
|
||||
13 | incorrect_set = {1, 1}
|
||||
| ^ B033
|
||||
14 | incorrect_set = {1, 1,}
|
||||
15 | incorrect_set = {0, 1, 1,}
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | "value1",
|
||||
11 11 | # B033
|
||||
12 12 | }
|
||||
13 |-incorrect_set = {1, 1}
|
||||
13 |+incorrect_set = {1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
|
||||
B033.py:14:21: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
12 | }
|
||||
13 | incorrect_set = {1, 1}
|
||||
14 | incorrect_set = {1, 1,}
|
||||
| ^ B033
|
||||
15 | incorrect_set = {0, 1, 1,}
|
||||
16 | incorrect_set = {0, 1, 1}
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | # B033
|
||||
12 12 | }
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 |-incorrect_set = {1, 1,}
|
||||
14 |+incorrect_set = {1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
17 17 | incorrect_set = {
|
||||
|
||||
B033.py:15:24: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
13 | incorrect_set = {1, 1}
|
||||
14 | incorrect_set = {1, 1,}
|
||||
15 | incorrect_set = {0, 1, 1,}
|
||||
| ^ B033
|
||||
16 | incorrect_set = {0, 1, 1}
|
||||
17 | incorrect_set = {
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | }
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 |-incorrect_set = {0, 1, 1,}
|
||||
15 |+incorrect_set = {0, 1,}
|
||||
16 16 | incorrect_set = {0, 1, 1}
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
|
||||
B033.py:16:24: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
14 | incorrect_set = {1, 1,}
|
||||
15 | incorrect_set = {0, 1, 1,}
|
||||
16 | incorrect_set = {0, 1, 1}
|
||||
| ^ B033
|
||||
17 | incorrect_set = {
|
||||
18 | 0,
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | incorrect_set = {1, 1}
|
||||
14 14 | incorrect_set = {1, 1,}
|
||||
15 15 | incorrect_set = {0, 1, 1,}
|
||||
16 |-incorrect_set = {0, 1, 1}
|
||||
16 |+incorrect_set = {0, 1}
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
19 19 | 1,
|
||||
|
||||
B033.py:20:5: B033 [*] Sets should not contain duplicate item `1`
|
||||
|
|
||||
18 | 0,
|
||||
19 | 1,
|
||||
20 | 1,
|
||||
| ^ B033
|
||||
21 | }
|
||||
|
|
||||
= help: Remove duplicate item
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 | incorrect_set = {
|
||||
18 18 | 0,
|
||||
19 19 | 1,
|
||||
20 |- 1,
|
||||
21 20 | }
|
||||
22 21 |
|
||||
23 22 | ###
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::super::helpers::shadows_builtin;
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
/// [`lint.flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -44,7 +44,7 @@ use super::super::helpers::shadows_builtin;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
/// - `lint.flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// ```
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// [`lint.flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// converted to the appropriate dunder method. Methods decorated with
|
||||
/// `@typing.override` or `@typing_extensions.override` are also
|
||||
/// ignored.
|
||||
@@ -55,7 +55,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
/// - `lint.flake8-builtins.builtins-ignorelist`
|
||||
#[violation]
|
||||
pub struct BuiltinAttributeShadowing {
|
||||
kind: Kind,
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
/// [`lint.flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -41,7 +41,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
/// - `lint.flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::iter;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
use libcst_native::{
|
||||
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
|
||||
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
|
||||
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
|
||||
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
|
||||
ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
|
||||
RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace,
|
||||
TrailingWhitespace, Tuple,
|
||||
};
|
||||
use std::iter;
|
||||
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -24,77 +25,10 @@ use crate::{
|
||||
checkers::ast::Checker,
|
||||
cst::matchers::{
|
||||
match_arg, match_call, match_call_mut, match_expression, match_generator_exp, match_lambda,
|
||||
match_list_comp, match_name, match_tuple,
|
||||
match_list_comp, match_tuple,
|
||||
},
|
||||
};
|
||||
|
||||
/// (C400) Convert `list(x for x in y)` to `[x for x in y]`.
|
||||
pub(crate) fn fix_unnecessary_generator_list(
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let generator_exp = match_generator_exp(&arg.value)?;
|
||||
|
||||
tree = Expression::ListComp(Box::new(ListComp {
|
||||
elt: generator_exp.elt.clone(),
|
||||
for_in: generator_exp.for_in.clone(),
|
||||
lbracket: LeftSquareBracket {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbracket: RightSquareBracket {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: generator_exp.lpar.clone(),
|
||||
rpar: generator_exp.rpar.clone(),
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
|
||||
pub(crate) fn fix_unnecessary_generator_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let generator_exp = match_generator_exp(&arg.value)?;
|
||||
|
||||
tree = Expression::SetComp(Box::new(SetComp {
|
||||
elt: generator_exp.elt.clone(),
|
||||
for_in: generator_exp.for_in.clone(),
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: generator_exp.lpar.clone(),
|
||||
rpar: generator_exp.rpar.clone(),
|
||||
}));
|
||||
|
||||
let content = tree.codegen_stylist(stylist);
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(content, expr.range(), checker.locator(), checker.semantic()),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
|
||||
/// range(3)}`.
|
||||
pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
@@ -151,46 +85,6 @@ pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) ->
|
||||
))
|
||||
}
|
||||
|
||||
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
|
||||
pub(crate) fn fix_unnecessary_list_comprehension_set(
|
||||
expr: &Expr,
|
||||
checker: &Checker,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
// Expr(Call(ListComp)))) ->
|
||||
// Expr(SetComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let list_comp = match_list_comp(&arg.value)?;
|
||||
|
||||
tree = Expression::SetComp(Box::new(SetComp {
|
||||
elt: list_comp.elt.clone(),
|
||||
for_in: list_comp.for_in.clone(),
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: list_comp.lpar.clone(),
|
||||
rpar: list_comp.rpar.clone(),
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in
|
||||
/// range(3)}`.
|
||||
pub(crate) fn fix_unnecessary_list_comprehension_dict(
|
||||
@@ -251,95 +145,6 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
|
||||
))
|
||||
}
|
||||
|
||||
/// Drop a trailing comma from a list of tuple elements.
|
||||
fn drop_trailing_comma<'a>(
|
||||
tuple: &Tuple<'a>,
|
||||
) -> Result<(
|
||||
Vec<Element<'a>>,
|
||||
ParenthesizableWhitespace<'a>,
|
||||
ParenthesizableWhitespace<'a>,
|
||||
)> {
|
||||
let whitespace_after = tuple
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after
|
||||
.clone();
|
||||
let whitespace_before = tuple
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before
|
||||
.clone();
|
||||
|
||||
let mut elements = tuple.elements.clone();
|
||||
if elements.len() == 1 {
|
||||
if let Some(Element::Simple {
|
||||
value,
|
||||
comma: Some(..),
|
||||
..
|
||||
}) = elements.last()
|
||||
{
|
||||
if whitespace_before == ParenthesizableWhitespace::default()
|
||||
&& whitespace_after == ParenthesizableWhitespace::default()
|
||||
{
|
||||
elements[0] = Element::Simple {
|
||||
value: value.clone(),
|
||||
comma: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((elements, whitespace_after, whitespace_before))
|
||||
}
|
||||
|
||||
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
|
||||
pub(crate) fn fix_unnecessary_literal_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(List|Tuple)))) -> Expr(Set)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => drop_trailing_comma(inner)?,
|
||||
Expression::List(inner) => (
|
||||
inner.elements.clone(),
|
||||
inner.lbracket.whitespace_after.clone(),
|
||||
inner.rbracket.whitespace_before.clone(),
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
|
||||
if elements.is_empty() {
|
||||
call.args = vec![];
|
||||
} else {
|
||||
tree = Expression::Set(Box::new(Set {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace { whitespace_after },
|
||||
rbrace: RightCurlyBrace { whitespace_before },
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
|
||||
pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
@@ -408,126 +213,82 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
|
||||
))
|
||||
}
|
||||
|
||||
/// (C408)
|
||||
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
enum Collection {
|
||||
Tuple,
|
||||
List,
|
||||
Dict,
|
||||
}
|
||||
|
||||
/// (C408) Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
|
||||
pub(crate) fn fix_unnecessary_collection_call(
|
||||
expr: &ast::ExprCall,
|
||||
checker: &Checker,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
|
||||
// Expr(Call("dict")))) -> Expr(Dict)
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call(&tree)?;
|
||||
let name = match_name(&call.func)?;
|
||||
let collection = match name.value {
|
||||
"tuple" => Collection::Tuple,
|
||||
"list" => Collection::List,
|
||||
"dict" => Collection::Dict,
|
||||
_ => bail!("Expected 'tuple', 'list', or 'dict'"),
|
||||
};
|
||||
|
||||
// Arena allocator used to create formatted strings of sufficient lifetime,
|
||||
// below.
|
||||
let mut arena: Vec<String> = vec![];
|
||||
|
||||
match collection {
|
||||
Collection::Tuple => {
|
||||
tree = Expression::Tuple(Box::new(Tuple {
|
||||
elements: vec![],
|
||||
lpar: vec![LeftParen::default()],
|
||||
rpar: vec![RightParen::default()],
|
||||
}));
|
||||
}
|
||||
Collection::List => {
|
||||
tree = Expression::List(Box::new(List {
|
||||
elements: vec![],
|
||||
lbracket: LeftSquareBracket::default(),
|
||||
rbracket: RightSquareBracket::default(),
|
||||
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
|
||||
|
||||
// Quote each argument.
|
||||
for arg in &call.args {
|
||||
let quoted = format!(
|
||||
"{}{}{}",
|
||||
quote,
|
||||
arg.keyword
|
||||
.as_ref()
|
||||
.expect("Expected dictionary argument to be kwarg")
|
||||
.value,
|
||||
quote,
|
||||
);
|
||||
arena.push(quoted);
|
||||
}
|
||||
|
||||
let elements = call
|
||||
.args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| DictElement::Simple {
|
||||
key: Expression::SimpleString(Box::new(SimpleString {
|
||||
value: &arena[i],
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
Collection::Dict => {
|
||||
if call.args.is_empty() {
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements: vec![],
|
||||
lbrace: LeftCurlyBrace::default(),
|
||||
rbrace: RightCurlyBrace::default(),
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
} else {
|
||||
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
|
||||
})),
|
||||
value: arg.value.clone(),
|
||||
comma: arg.comma.clone(),
|
||||
whitespace_before_colon: ParenthesizableWhitespace::default(),
|
||||
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
|
||||
" ",
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Quote each argument.
|
||||
for arg in &call.args {
|
||||
let quoted = format!(
|
||||
"{}{}{}",
|
||||
quote,
|
||||
arg.keyword
|
||||
.as_ref()
|
||||
.expect("Expected dictionary argument to be kwarg")
|
||||
.value,
|
||||
quote,
|
||||
);
|
||||
arena.push(quoted);
|
||||
}
|
||||
|
||||
let elements = call
|
||||
.args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| DictElement::Simple {
|
||||
key: Expression::SimpleString(Box::new(SimpleString {
|
||||
value: &arena[i],
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
})),
|
||||
value: arg.value.clone(),
|
||||
comma: arg.comma.clone(),
|
||||
whitespace_before_colon: ParenthesizableWhitespace::default(),
|
||||
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
|
||||
SimpleWhitespace(" "),
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: call
|
||||
.args
|
||||
.last()
|
||||
.expect("Arguments should be non-empty")
|
||||
.whitespace_after_arg
|
||||
.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: call
|
||||
.args
|
||||
.last()
|
||||
.expect("Arguments should be non-empty")
|
||||
.whitespace_after_arg
|
||||
.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
if matches!(collection, Collection::Dict) {
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
)
|
||||
} else {
|
||||
tree.codegen_stylist(stylist)
|
||||
},
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
@@ -544,7 +305,7 @@ pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) ->
|
||||
/// However, this is a syntax error under the f-string grammar. As such,
|
||||
/// this method will pad the start and end of an expression as needed to
|
||||
/// avoid producing invalid syntax.
|
||||
fn pad_expression(
|
||||
pub(crate) fn pad_expression(
|
||||
content: String,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
@@ -575,106 +336,46 @@ fn pad_expression(
|
||||
}
|
||||
}
|
||||
|
||||
/// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)`
|
||||
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
|
||||
expr: &Expr,
|
||||
/// Like [`pad_expression`], but only pads the start of the expression.
|
||||
pub(crate) fn pad_start(
|
||||
content: &str,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => (
|
||||
&inner.elements,
|
||||
&inner
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after,
|
||||
&inner
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before,
|
||||
),
|
||||
Expression::List(inner) => (
|
||||
&inner.elements,
|
||||
&inner.lbracket.whitespace_after,
|
||||
&inner.rbracket.whitespace_before,
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
semantic: &SemanticModel,
|
||||
) -> String {
|
||||
if !semantic.in_f_string() {
|
||||
return content.into();
|
||||
}
|
||||
|
||||
tree = Expression::Tuple(Box::new(Tuple {
|
||||
elements: elements.clone(),
|
||||
lpar: vec![LeftParen {
|
||||
whitespace_after: whitespace_after.clone(),
|
||||
}],
|
||||
rpar: vec![RightParen {
|
||||
whitespace_before: whitespace_before.clone(),
|
||||
}],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let prefix = locator.up_to(range.start());
|
||||
if matches!(prefix.chars().next_back(), Some('{')) {
|
||||
format!(" {content}")
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// (C410) Convert `list([1, 2])` to `[1, 2]`
|
||||
pub(crate) fn fix_unnecessary_literal_within_list_call(
|
||||
expr: &Expr,
|
||||
/// Like [`pad_expression`], but only pads the end of the expression.
|
||||
pub(crate) fn pad_end(
|
||||
content: &str,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => (
|
||||
&inner.elements,
|
||||
&inner
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after,
|
||||
&inner
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before,
|
||||
),
|
||||
Expression::List(inner) => (
|
||||
&inner.elements,
|
||||
&inner.lbracket.whitespace_after,
|
||||
&inner.rbracket.whitespace_before,
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
semantic: &SemanticModel,
|
||||
) -> String {
|
||||
if !semantic.in_f_string() {
|
||||
return content.into();
|
||||
}
|
||||
|
||||
tree = Expression::List(Box::new(List {
|
||||
elements: elements.clone(),
|
||||
lbracket: LeftSquareBracket {
|
||||
whitespace_after: whitespace_after.clone(),
|
||||
},
|
||||
rbracket: RightSquareBracket {
|
||||
whitespace_before: whitespace_before.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let suffix = locator.after(range.end());
|
||||
if matches!(suffix.chars().next(), Some('}')) {
|
||||
format!("{content} ")
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`.
|
||||
@@ -1091,25 +792,6 @@ pub(crate) fn fix_unnecessary_map(
|
||||
Ok(Edit::range_replacement(content, expr.range()))
|
||||
}
|
||||
|
||||
/// (C418) Convert `dict({"a": 1})` to `{"a": 1}`
|
||||
pub(crate) fn fix_unnecessary_literal_within_dict_call(
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
tree = arg.value.clone();
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C419) Convert `[i for i in a]` into `i for i in a`
|
||||
pub(crate) fn fix_unnecessary_comprehension_any_all(
|
||||
expr: &Expr,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
|
||||
/// ## What it does
|
||||
@@ -36,6 +36,9 @@ use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
|
||||
/// when rewriting the call. In most cases, though, comments will be preserved.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments`
|
||||
#[violation]
|
||||
pub struct UnnecessaryCollectionCall {
|
||||
obj_type: String,
|
||||
@@ -56,42 +59,88 @@ impl AlwaysFixableViolation for UnnecessaryCollectionCall {
|
||||
/// C408
|
||||
pub(crate) fn unnecessary_collection_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
call: &ast::ExprCall,
|
||||
settings: &Settings,
|
||||
) {
|
||||
if !args.is_empty() {
|
||||
if !call.arguments.args.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(func) = func.as_name_expr() else {
|
||||
let Some(func) = call.func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
match func.id.as_str() {
|
||||
let collection = match func.id.as_str() {
|
||||
"dict"
|
||||
if keywords.is_empty()
|
||||
if call.arguments.keywords.is_empty()
|
||||
|| (!settings.allow_dict_calls_with_keyword_arguments
|
||||
&& keywords.iter().all(|kw| kw.arg.is_some())) =>
|
||||
&& call.arguments.keywords.iter().all(|kw| kw.arg.is_some())) =>
|
||||
{
|
||||
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
|
||||
Collection::Dict
|
||||
}
|
||||
"list" | "tuple" => {
|
||||
// `list()` or `tuple()`
|
||||
"list" if call.arguments.keywords.is_empty() => {
|
||||
// `list()
|
||||
Collection::List
|
||||
}
|
||||
"tuple" if call.arguments.keywords.is_empty() => {
|
||||
// `tuple()`
|
||||
Collection::Tuple
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if !checker.semantic().is_builtin(func.id.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryCollectionCall {
|
||||
obj_type: func.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_collection_call(expr, checker).map(Fix::unsafe_edit)
|
||||
});
|
||||
|
||||
// Convert `dict()` to `{}`.
|
||||
if call.arguments.keywords.is_empty() {
|
||||
diagnostic.set_fix({
|
||||
// Replace from the start of the call to the start of the argument.
|
||||
let call_start = Edit::replacement(
|
||||
match collection {
|
||||
Collection::Dict => {
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic())
|
||||
}
|
||||
Collection::List => "[".to_string(),
|
||||
Collection::Tuple => "(".to_string(),
|
||||
},
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner list or tuple to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
match collection {
|
||||
Collection::Dict => {
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic())
|
||||
}
|
||||
Collection::List => "]".to_string(),
|
||||
Collection::Tuple => ")".to_string(),
|
||||
},
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
} else {
|
||||
// Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_collection_call(call, checker).map(Fix::unsafe_edit)
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
enum Collection {
|
||||
Tuple,
|
||||
List,
|
||||
Dict,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
@@ -47,27 +44,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorList {
|
||||
}
|
||||
|
||||
/// C400 (`list(generator)`)
|
||||
pub(crate) fn unnecessary_generator_list(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("list", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"list",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("list") {
|
||||
return;
|
||||
}
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_list(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
if argument.is_generator_exp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, call.range());
|
||||
|
||||
// Convert `list(x for x in y)` to `[x for x in y]`.
|
||||
diagnostic.set_fix({
|
||||
// Replace `list(` with `[`.
|
||||
let call_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let call_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -47,26 +45,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorSet {
|
||||
}
|
||||
|
||||
/// C401 (`set(generator)`)
|
||||
pub(crate) fn unnecessary_generator_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
return;
|
||||
}
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_set(expr, checker).map(Fix::unsafe_edit)
|
||||
if argument.is_generator_exp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, call.range());
|
||||
|
||||
// Convert `set(x for x in y)` to `{x for x in y}`.
|
||||
diagnostic.set_fix({
|
||||
// Replace `set(` with `}`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -45,25 +43,43 @@ impl AlwaysFixableViolation for UnnecessaryListComprehensionSet {
|
||||
}
|
||||
|
||||
/// C403 (`set([...])`)
|
||||
pub(crate) fn unnecessary_list_comprehension_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_list_comprehension_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
return;
|
||||
}
|
||||
if argument.is_list_comp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_set(expr, checker).map(Fix::unsafe_edit)
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range());
|
||||
diagnostic.set_fix({
|
||||
// Replace `set(` with `{`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
// Delete the open bracket (`[`).
|
||||
let argument_start =
|
||||
Edit::deletion(argument.start(), argument.start() + TextSize::from(1));
|
||||
|
||||
// Delete the close bracket (`]`).
|
||||
let argument_end = Edit::deletion(argument.end() - TextSize::from(1), argument.end());
|
||||
|
||||
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -53,16 +51,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralSet {
|
||||
}
|
||||
|
||||
/// C405 (`set([1, 2])`)
|
||||
pub(crate) fn unnecessary_literal_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_literal_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
@@ -73,13 +68,69 @@ pub(crate) fn unnecessary_literal_set(
|
||||
Expr::Tuple(_) => "tuple",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralSet {
|
||||
obj_type: kind.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic
|
||||
.try_set_fix(|| fixes::fix_unnecessary_literal_set(expr, checker).map(Fix::unsafe_edit));
|
||||
|
||||
// Convert `set((1, 2))` to `{1, 2}`.
|
||||
diagnostic.set_fix({
|
||||
let elts = match &argument {
|
||||
Expr::List(ast::ExprList { elts, .. }) => elts,
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match elts.as_slice() {
|
||||
// If the list or tuple is empty, replace the entire call with `set()`.
|
||||
[] => Fix::unsafe_edit(Edit::range_replacement("set()".to_string(), call.range())),
|
||||
// If it's a single-element tuple (with no whitespace around it), remove the trailing
|
||||
// comma.
|
||||
[elt]
|
||||
if argument.is_tuple_expr()
|
||||
// The element must start right after the `(`.
|
||||
&& elt.start() == argument.start() + TextSize::new(1)
|
||||
// The element must be followed by exactly one comma and a closing `)`.
|
||||
&& elt.end() + TextSize::new(2) == argument.end() =>
|
||||
{
|
||||
// Replace from the start of the call to the start of the inner element.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
elt.start(),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner element to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
elt.end(),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
_ => {
|
||||
// Replace from the start of the call to the start of the inner list or tuple with `{`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
argument.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner list or tuple to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
argument.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -68,17 +66,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinDictCall {
|
||||
}
|
||||
|
||||
/// C418
|
||||
pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
if !keywords.is_empty() {
|
||||
pub(crate) fn unnecessary_literal_within_dict_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(argument) = helpers::first_argument_with_matching_function("dict", func, args) else {
|
||||
let Some(argument) =
|
||||
helpers::first_argument_with_matching_function("dict", &call.func, &call.arguments.args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("dict") {
|
||||
@@ -89,15 +83,24 @@ pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
Expr::Dict(_) => DictKind::Literal,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralWithinDictCall {
|
||||
kind: argument_kind,
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_dict_call(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
|
||||
// Convert `dict({"a": 1})` to `{"a": 1}`
|
||||
diagnostic.set_fix({
|
||||
// Delete from the start of the call to the start of the argument.
|
||||
let call_start = Edit::deletion(call.start(), argument.start());
|
||||
|
||||
// Delete from the end of the argument to the end of the call.
|
||||
let call_end = Edit::deletion(argument.end(), call.end());
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
@@ -70,17 +68,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinListCall {
|
||||
}
|
||||
|
||||
/// C410
|
||||
pub(crate) fn unnecessary_literal_within_list_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
if !keywords.is_empty() {
|
||||
pub(crate) fn unnecessary_literal_within_list_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else {
|
||||
let Some(argument) =
|
||||
helpers::first_argument_with_matching_function("list", &call.func, &call.arguments.args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("list") {
|
||||
@@ -91,15 +85,43 @@ pub(crate) fn unnecessary_literal_within_list_call(
|
||||
Expr::List(_) => "list",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralWithinListCall {
|
||||
literal: argument_kind.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_list_call(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
|
||||
// Convert `list([1, 2])` to `[1, 2]`
|
||||
diagnostic.set_fix({
|
||||
// Delete from the start of the call to the start of the argument.
|
||||
let call_start = Edit::deletion(call.start(), argument.start());
|
||||
|
||||
// Delete from the end of the argument to the end of the call.
|
||||
let call_end = Edit::deletion(argument.end(), call.end());
|
||||
|
||||
// If this is a tuple, we also need to convert the inner argument to a list.
|
||||
if argument.is_tuple_expr() {
|
||||
// Replace `(` with `[`.
|
||||
let argument_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
argument.start(),
|
||||
argument.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let argument_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
argument.end() - TextSize::from(1),
|
||||
argument.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
|
||||
} else {
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user