Compare commits

..

23 Commits

Author SHA1 Message Date
Zanie
b75864c5f9 WIP: Nightmare scenario warning on redirected deprecated rules 2024-01-31 10:59:11 -06:00
Zanie
4a5d711a6e Fix bug where selection included deprecated rules during preview 2024-01-30 16:04:05 -06:00
Zanie
204c000e20 Track deprecated rules during selection 2024-01-30 16:01:58 -06:00
Zanie
27f2b3af5d Additional cleanup 2024-01-30 15:39:18 -06:00
Zanie
4d5d889055 Change phrasing 2024-01-30 15:36:18 -06:00
Zanie
427a05b1b5 Refactor 2024-01-30 15:33:55 -06:00
Zanie
7e763cf84d Lint 2024-01-30 13:51:18 -06:00
Zanie
e5bf2301bb Add handling for rules that are both deprecated and redirected 2024-01-30 13:11:31 -06:00
Zanie
c7e40e6dc1 Mark PGH rules as deprecated 2024-01-30 12:12:39 -06:00
Zanie Blue
761e148308 Add rule removal infrastructure (#9691)
Similar to https://github.com/astral-sh/ruff/pull/9689 — retains removed
rules for better error messages and documentation but removed rules
_cannot_ be used in any context.

Removes PLR1706 as a useful test case and something we want to
accomplish in #9680 anyway. The rule was in preview so we do not need to
deprecate it first.

Closes https://github.com/astral-sh/ruff/issues/9007

## Test plan

<img width="1110" alt="Rules table"
src="https://github.com/astral-sh/ruff/assets/2586601/ac9fa682-623c-44aa-8e51-d8ab0d308355">

<img width="1110" alt="Rule page"
src="https://github.com/astral-sh/ruff/assets/2586601/05850b2d-7ca5-49bb-8df8-bb931bab25cd">
2024-01-30 12:04:06 -06:00
Zanie Blue
5c75da980f Add rule deprecation infrastructure (#9689)
Adds a new `Deprecated` rule group in addition to `Stable` and
`Preview`.

Deprecated rules:
- Warn on explicit selection without preview
- Error on explicit selection with preview
- Are excluded when selected by prefix with preview

Deprecates `TRY200`, `ANN101`, and `ANN102` as a proof of concept. We
can consider deprecating them separately.
2024-01-30 12:04:06 -06:00
Zanie Blue
747728beca Remove the NURSERY selector from the json schema (#9695) 2024-01-30 12:04:06 -06:00
Zanie Blue
897b7f75b9 Always request the concise output format during ecosystem checks (#9708)
Fixes a regression in the ecosystem checks from
https://github.com/astral-sh/ruff/pull/9687 which was causing them to
run for multiple hours due to the size of the output.

We need the concise format for comparisons.

We should probably update the ecosystem checks to actually diff the full
output in the future because that'd be nice.
2024-01-30 12:04:06 -06:00
Zanie Blue
07c6a3e672 Error if nursery rules are selected without preview (#9683)
Extends #9682 to error if the nursery selector is used or nursery rules
are selected without preview.

Part of #7992 — we will remove this in 0.3.0 instead so we can provide
nice errors in 0.2.0.
2024-01-30 12:04:06 -06:00
Jane Lewis
57576fa139 Replace --show-source and --no-show-source with --output_format=<full|concise> (#9687)
Fixes #7350

## Summary

* `--show-source` and `--no-show-source` are now deprecated.
* `output-format` supports two new variants, `full` and `concise`.
`text` is now a deprecated variant, and any use of it is treated as the
default serialization format.
* `--output-format` now default to `concise`
* In preview mode, `--output-format` defaults to `full`
* `--show-source` will still set `--output-format` to `full` if the
output format is not otherwise specified.
* likewise, `--no-show-source` can override an output format that was
set in a file-based configuration, though it will also be overridden by
`--output-format`

## Test Plan

A lot of tests were updated to use `--output-format=full`. Additional
tests were added to ensure the correct deprecation warnings appeared,
and that deprecated options behaved as intended.
2024-01-30 12:04:06 -06:00
Charlie Marsh
b1c793e2ad Remove preview gating for flake8-simplify rules (#9686)
## Summary

Un-gates detecting `dict.get` rewrites in `if` expressions (rather than
just `if` statements).
2024-01-30 12:04:06 -06:00
Charlie Marsh
9fca4d830e Remove preview gating for flake8-pie rules (#9684)
## Summary

Both of the preview behaviors gated here seem like improvements, so
let's make them stable in v0.2.0
2024-01-30 12:04:06 -06:00
Charlie Marsh
d318a27b0f Remove preview gating for pycodestyle rules (#9685)
## Summary

Un-gates the behavior to allow `sys.path` modifications between imports,
which removed a bunch of false positives in the ecosystem CI at the
time.
2024-01-30 12:04:06 -06:00
Zanie
002d040c41 Remove preview gating for newly-added stable fixes (#9681)
## Summary

At present, our versioning policy forbids the addition of safe fixes to
stable rules outside of a minor release, so we've accumulated a bunch of
new fixes that are behind `--preview`, and can be ungated in v0.2.0.

To find these, I just grepped for `preview.is_enabled()` and identified
all such cases. I then audited the `preview_rules` test fixtures and
removed any tests that existed only to test this autofix behavior.
# Conflicts:
#	crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_SIM114.py.snap
#	crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM114_SIM114.py.snap
2024-01-30 12:04:02 -06:00
Charlie Marsh
432b4da27b Recategorize static-key-dict-comprehension from RUF011 to B035 (#9428)
## Summary

This rule was added to flake8-bugbear. In general, we tend to prefer
redirecting to prominent plugins when our own rules are reimplemented
(since more projects have `B` activated than `RUF`).

## Test Plan

`cargo test`
2024-01-30 12:00:45 -06:00
Charlie Marsh
7992caed37 [flake8-pyi] Mark unaliased-collections-abc-set-import fix as safe (#9679)
## Summary

Prompted by
https://github.com/astral-sh/ruff/issues/8482#issuecomment-1859299411.
The rename is only unsafe when the symbol is exported, so we can narrow
the conditions.
2024-01-30 12:00:45 -06:00
Micha Reiser
4c8d1fe0e4 Add deprecation message for top-level lint settings (#9582) 2024-01-30 12:00:45 -06:00
Micha Reiser
8e8c632409 Promote lint. settings over top-level settings (#9476) 2024-01-30 12:00:45 -06:00
1327 changed files with 44209 additions and 80643 deletions

View File

@@ -1,8 +0,0 @@
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).
failure-output = "immediate-final"
# Do not cancel the test run on the first failure.
fail-fast = false
status-level = "skip"

2
.gitattributes vendored
View File

@@ -2,8 +2,6 @@
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf

6
.github/CODEOWNERS vendored
View File

@@ -7,9 +7,3 @@
# Jupyter # Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila /crates/ruff_linter/src/jupyter/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood

View File

@@ -3,8 +3,6 @@ Thank you for taking the time to report an issue! We're glad to have you involve
If you're filing a bug report, please consider including the following information: If you're filing a bug report, please consider including the following information:
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
e.g. "RUF001", "unused variable", "Jupyter notebook"
* A minimal code snippet that reproduces the bug. * A minimal code snippet that reproduces the bug.
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag. * The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
* The current Ruff settings (any relevant sections from your `pyproject.toml`). * The current Ruff settings (any relevant sections from your `pyproject.toml`).

View File

@@ -9,10 +9,6 @@ updates:
actions: actions:
patterns: 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" - package-ecosystem: "cargo"
directory: "/" directory: "/"

View File

@@ -35,7 +35,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: tj-actions/changed-files@v42 - uses: tj-actions/changed-files@v41
id: changed id: changed
with: with:
files_yaml: | files_yaml: |
@@ -111,29 +111,19 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Install cargo insta" - name: "Install cargo insta"
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
tool: cargo-insta tool: cargo-insta
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: "Run tests" - name: "Run tests"
shell: bash run: cargo insta test --all --all-features --unreferenced reject
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# Check for broken links in the documentation. # Check for broken links in the documentation.
- run: cargo doc --all --no-deps - run: cargo doc --all --no-deps
env: env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: ruff name: ruff
path: target/debug/ruff path: target/debug/ruff
@@ -148,16 +138,15 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install cargo nextest" - name: "Install cargo insta"
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
tool: cargo-nextest tool: cargo-insta
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: "Run tests" - name: "Run tests"
shell: bash shell: bash
run: | # We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
cargo nextest run --all-features --profile ci run: cargo insta test --all --all-features
cargo test --all-features --doc
cargo-test-wasm: cargo-test-wasm:
name: "cargo test (wasm)" name: "cargo test (wasm)"
@@ -238,7 +227,7 @@ jobs:
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
name: Download comparison Ruff binary name: Download comparison Ruff binary
id: ruff-target id: ruff-target
with: with:
@@ -250,7 +239,6 @@ jobs:
with: with:
name: ruff name: ruff
branch: ${{ github.event.pull_request.base.ref }} branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true check_artifacts: true
- name: Install ruff-ecosystem - name: Install ruff-ecosystem
@@ -325,13 +313,13 @@ jobs:
run: | run: |
echo ${{ github.event.number }} > pr-number echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
name: Upload PR Number name: Upload PR Number
with: with:
name: pr-number name: pr-number
path: pr-number path: pr-number
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
name: Upload Results name: Upload Results
with: with:
name: ecosystem-result name: ecosystem-result
@@ -394,7 +382,7 @@ jobs:
- name: "Install pre-commit" - name: "Install pre-commit"
run: pip install pre-commit run: pip install pre-commit
- name: "Cache pre-commit" - name: "Cache pre-commit"
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -419,7 +407,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
- name: "Add SSH key" - name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.8.0
with: with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -472,7 +460,7 @@ jobs:
- determine_changes - determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps: steps:
- uses: extractions/setup-just@v2 - uses: extractions/setup-just@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -485,7 +473,7 @@ jobs:
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
name: Download development ruff binary name: Download development ruff binary
id: ruff-target id: ruff-target
with: with:

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
- name: "Add SSH key" - name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.8.0
with: with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"

View File

@@ -61,7 +61,7 @@ jobs:
echo 'EOF' >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT
- name: Find existing comment - name: Find existing comment
uses: peter-evans/find-comment@v3 uses: peter-evans/find-comment@v2
if: steps.generate-comment.outcome == 'success' if: steps.generate-comment.outcome == 'success'
id: find-comment id: find-comment
with: with:
@@ -71,7 +71,7 @@ jobs:
- name: Create or update comment - name: Create or update comment
if: steps.find-comment.outcome == 'success' if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }} issue-number: ${{ steps.pr-number.outputs.pr-number }}

View File

@@ -52,9 +52,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload sdist" - name: "Upload sdist"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-sdist name: wheels
path: dist path: dist
macos-x86_64: macos-x86_64:
@@ -80,9 +80,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-macos-x86_64 name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -90,9 +90,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-macos-x86_64 name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -119,9 +119,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-aarch64-apple-darwin name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -129,9 +129,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-aarch64-apple-darwin name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -170,9 +170,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
shell: bash shell: bash
@@ -181,9 +181,9 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe 7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.zip *.zip
*.sha256 *.sha256
@@ -218,9 +218,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -228,9 +228,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -251,12 +251,8 @@ jobs:
arch: s390x arch: s390x
- target: powerpc64le-unknown-linux-gnu - target: powerpc64le-unknown-linux-gnu
arch: ppc64le arch: ppc64le
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu - target: powerpc64-unknown-linux-gnu
arch: ppc64 arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -289,9 +285,9 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -299,9 +295,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -341,9 +337,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help .venv/bin/ruff check --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -351,9 +347,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -398,9 +394,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help .venv/bin/ruff check --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
@@ -408,9 +404,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -467,11 +463,10 @@ jobs:
# For pypi trusted publishing # For pypi trusted publishing
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
with: with:
pattern: wheels-* name: wheels
path: wheels path: wheels
merge-multiple: true
- name: Publish to PyPi - name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
@@ -511,13 +506,12 @@ jobs:
# For GitHub release publishing # For GitHub release publishing
contents: write contents: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
with: with:
pattern: binaries-* name: binaries
path: binaries path: binaries
merge-multiple: true
- name: "Publish to GitHub" - name: "Publish to GitHub"
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
draft: true draft: true
files: binaries/* files: binaries/*

View File

@@ -1,56 +1,5 @@
# Breaking Changes # Breaking Changes
## 0.3.0
### Ruff 2024.2 style
The formatter now formats code according to the Ruff 2024.2 style guide. Read the [changelog](./CHANGELOG.md#030) for a detailed list of stabilized style changes.
### `isort`: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
Previously, Ruff used one or two blank lines (or the number configured by `isort.lines-after-imports`) after imports in typing stub files (`.pyi` files).
The [typing style guide for stubs](https://typing.readthedocs.io/en/latest/source/stubs.html#style-guide) recommends using at most 1 blank line for grouping.
As of this release, `isort` now always uses one blank line after imports in stub files, the same as the formatter.
### `build` is no longer excluded by default ([#10093](https://github.com/astral-sh/ruff/pull/10093))
Ruff maintains a list of directories and files that are excluded by default. This list now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git`
- `.git-rewrite`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `build` directory was included in this list. However, the `build` directory tends to be a not-unpopular directory
name, and excluding it by default caused confusion. Ruff now no longer excludes `build` except if it is excluded by a `.gitignore` file
or because it is listed in `extend-exclude`.
### `--format` is no longer a valid `rule` or `linter` command option
Previously, `ruff rule` and `ruff linter` accepted the `--format <FORMAT>` option as an alias for `--output-format`. Ruff no longer
supports this alias. Please use `ruff rule --output-format <FORMAT>` and `ruff linter --output-format <FORMAT>` instead.
## 0.1.9 ## 0.1.9
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513)) ### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -1,396 +1,5 @@
# Changelog # Changelog
## 0.3.2
### Preview features
- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
### Rule changes
- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263))
- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254))
### Bug fixes
- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297))
- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274))
- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265))
- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280))
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285))
## 0.3.1
### Preview features
- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117))
- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096))
- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098))
- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140))
- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176))
### Rule changes
- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194))
- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238))
- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215))
### CLI
- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110))
- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150))
- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219))
### Configuration
- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216))
- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149))
### Bug fixes
- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220))
- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203))
- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206))
- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587))
- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178))
## 0.3.0
This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.
### Preview features
- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154))
- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066))
- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094))
### Rule changes
- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055))
- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057))
- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027))
- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065))
- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845))
- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054))
- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060))
- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148))
### Formatter
This release introduces the Ruff 2024.2 style, stabilizing the following changes:
- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154))
- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222))
- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155))
- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240))
- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283))
- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210))
- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637))
- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280))
- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### CLI
- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135))
- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093))
- Deprecate `ruff <path>`, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check <path>`, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169))
- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170))
### Bug fixes
- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152))
- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091))
- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049))
- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162))
- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074))
- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118))
- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080))
### Performance
- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121))
- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116))
### Documentation
- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144))
- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136))
## 0.2.2
Highlights include:
- Initial support formatting f-strings (in `--preview`).
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config`
argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
### Preview features
- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642))
- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266))
- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880))
### Rule changes
- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793))
- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862))
- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887))
- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976))
- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952))
### Formatter
- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915))
- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957))
- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922))
### CLI
- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599))
### Bug fixes
- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866))
- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905))
- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969))
- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981))
- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953))
- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996))
- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859))
- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955))
- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907))
- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894))
- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878))
- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964))
- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956))
- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865))
- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990))
- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849))
### Performance
- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888))
- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853))
- Reduce `Result<Tok, LexicalError>` size by using `Box<str>` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885))
- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900))
- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867))
- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884))
## 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 ## 0.1.15
### Preview features ### Preview features

View File

@@ -26,10 +26,6 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
- [`cargo dev`](#cargo-dev) - [`cargo dev`](#cargo-dev)
- [Subsystems](#subsystems) - [Subsystems](#subsystems)
- [Compilation Pipeline](#compilation-pipeline) - [Compilation Pipeline](#compilation-pipeline)
- [Import Categorization](#import-categorization)
- [Project root](#project-root)
- [Package root](#package-root)
- [Import categorization](#import-categorization-1)
## The Basics ## The Basics
@@ -39,7 +35,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change. creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.com/invite/astral-sh) to discuss your idea with the You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug) in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted) and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
@@ -67,7 +63,7 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
cargo install cargo-insta cargo install cargo-insta
``` ```
And you'll need pre-commit to run some validation checks: and pre-commit to run some validation checks:
```shell ```shell
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
@@ -80,16 +76,6 @@ when making a commit:
pre-commit install pre-commit install
``` ```
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
though it's not strictly necessary:
```shell
cargo install cargo-nextest --locked
```
Throughout this guide, any usages of `cargo test` can be replaced with `cargo nextest run`,
if you choose to install `nextest`.
### Development ### Development
After cloning the repository, run Ruff locally from the repository root with: After cloning the repository, run Ruff locally from the repository root with:
@@ -245,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: For example, if you're adding a new rule named `E402`, you would run:
```shell ```shell
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402 cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
``` ```
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that **Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
@@ -316,7 +302,7 @@ To preview any changes to the documentation locally:
``` ```
The documentation should then be available locally at The documentation should then be available locally at
[http://127.0.0.1:8000/ruff/](http://127.0.0.1:8000/ruff/). [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
## Release Process ## Release Process
@@ -329,13 +315,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
### Creating a new release ### Creating a new release
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh` We use an experimental in-house tool for managing releases.
1. Run `./scripts/release/bump.sh`; this command will:
- Generate a temporary virtual environment with `rooster` 1. Install `rooster`: `pip install git+https://github.com/zanieb/rooster@main`
1. Run `rooster release`; this command will:
- Generate a changelog entry in `CHANGELOG.md` - Generate a changelog entry in `CHANGELOG.md`
- Update versions in `pyproject.toml` and `Cargo.toml` - Update versions in `pyproject.toml` and `Cargo.toml`
- Update references to versions in the `README.md` and documentation - Update references to versions in the `README.md` and documentation
- Display contributors for the release
1. The changelog should then be editorialized for consistency 1. The changelog should then be editorialized for consistency
- Often labels will be missing from pull requests they will need to be manually organized into the proper section - Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details - Changes should be edited to be user-facing descriptions, avoiding internal details
@@ -359,7 +345,7 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Open the draft release in the GitHub release section 1. Open the draft release in the GitHub release section
1. Copy the changelog for the release into the GitHub release 1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers - See previous releases for formatting of section headers
1. Append the contributors from the `bump.sh` script 1. Generate the contributor list with `rooster contributors` and add to the release notes
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py). 1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when 1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff. `git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
@@ -387,11 +373,6 @@ We have several ways of benchmarking and profiling Ruff:
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests. - Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests.
- Profiling the linter on either the microbenchmarks or entire projects - Profiling the linter on either the microbenchmarks or entire projects
> \[!NOTE\]
> When running benchmarks, ensure that your CPU is otherwise idle (e.g., close any background
> applications, like web browsers). You may also want to switch your CPU to a "performance"
> mode, if it exists, especially when benchmarking short-lived processes.
### CPython Benchmark ### CPython Benchmark
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase, First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,

1090
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,55 +14,49 @@ license = "MIT"
[workspace.dependencies] [workspace.dependencies]
aho-corasick = { version = "1.1.2" } aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] } annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.80" } anyhow = { version = "1.0.79" }
argfile = { version = "0.1.6" } argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" } assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" } bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" } bitflags = { version = "2.4.1" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" } cachedir = { version = "0.3.1" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] } chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.4.13", features = ["derive"] }
clap_complete_command = { version = "0.5.1" } clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" } clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.4.0", default-features = false } codspeed-criterion-compat = { version = "2.3.3", default-features = false }
colored = { version = "2.1.0" } colored = { version = "2.1.0" }
configparser = { version = "3.0.3" } configparser = { version = "3.0.3" }
console_error_panic_hook = { version = "0.1.7" } console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" } console_log = { version = "1.0.0" }
countme = { version = "3.0.1" } countme = { version ="3.0.1"}
criterion = { version = "0.5.1", default-features = false } criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dirs = { version = "5.0.0" } dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" } drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.10.1" } env_logger = { version ="0.10.1"}
fern = { version = "0.6.1" } fern = { version = "0.6.1" }
filetime = { version = "0.2.23" } filetime = { version = "0.2.23" }
fs-err = { version = "2.11.0" } fs-err = { version ="2.11.0"}
glob = { version = "0.3.1" } glob = { version = "0.3.1" }
globset = { version = "0.4.14" } globset = { version = "0.4.14" }
hexf-parse = { version = "0.2.1" } hexf-parse = { version ="0.2.1"}
ignore = { version = "0.4.22" } ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" } imara-diff ={ version = "0.1.5"}
imperative = { version = "1.0.4" } imperative = { version = "1.0.4" }
indicatif = { version = "0.17.8" } indicatif ={ version = "0.17.7"}
indoc = { version = "2.0.4" } indoc ={ version = "2.0.4"}
insta = { version = "1.35.1", feature = ["filters", "glob"] } insta = { version = "1.34.0", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" } insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.5" } is-macro = { version = "0.3.4" }
is-wsl = { version = "0.4.0" } is-wsl = { version = "0.4.0" }
itertools = { version = "0.12.1" } itertools = { version = "0.12.0" }
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.67" }
jod-thread = { version = "0.1.2" }
lalrpop-util = { version = "0.20.0", default-features = false } lalrpop-util = { version = "0.20.0", default-features = false }
lexical-parse-float = { version = "0.8.0", features = ["format"] } lexical-parse-float = { version = "0.8.0", features = ["format"] }
libc = { version = "0.2.153" }
libcst = { version = "1.1.0", default-features = false } libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" } log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" } memchr = { version = "2.6.4" }
lsp-types = { version = "0.95.0", features = ["proposed"] } mimalloc = { version ="0.1.39"}
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" } natord = { version = "1.0.9" }
notify = { version = "6.1.1" } notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" } once_cell = { version = "1.19.0" }
@@ -70,8 +64,8 @@ path-absolutize = { version = "3.1.1" }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.4.0", features = ["serde"] } pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.78" } proc-macro2 = { version = "1.0.73" }
pyproject-toml = { version = "0.9.0" } pyproject-toml = { version = "0.8.1" }
quick-junit = { version = "0.3.5" } quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" } quote = { version = "1.0.23" }
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
@@ -80,39 +74,39 @@ regex = { version = "1.10.2" }
result-like = { version = "0.5.0" } result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" } rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" } schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" } seahash = { version ="4.1.0"}
serde = { version = "1.0.197", features = ["derive"] } semver = { version = "1.0.21" }
serde-wasm-bindgen = { version = "0.6.4" } serde = { version = "1.0.195", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
serde_json = { version = "1.0.113" } serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" } serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] } serde_with = { version = "3.5.1", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" } shellexpand = { version = "3.0.0" }
shlex = { version = "1.3.0" } shlex = { version ="1.3.0"}
similar = { version = "2.4.0", features = ["inline"] } similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.1" } smallvec = { version = "1.13.1" }
static_assertions = "1.1.0" static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] } strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" } strum_macros = { version = "0.25.3" }
syn = { version = "2.0.51" } syn = { version = "2.0.40" }
tempfile = { version = "3.9.0" } tempfile = { version ="3.9.0"}
test-case = { version = "3.3.1" } test-case = { version = "3.3.1" }
thiserror = { version = "1.0.57" } thiserror = { version = "1.0.51" }
tikv-jemallocator = { version = "0.5.0" } tikv-jemallocator = { version ="0.5.0"}
toml = { version = "0.8.9" } toml = { version = "0.8.8" }
tracing = { version = "0.1.40" } tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" } tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-tree = { version = "0.2.4" }
typed-arena = { version = "2.0.2" } typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" } unic-ucd-category = { version ="0.9"}
unicode-ident = { version = "1.0.12" } unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" } unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.2" } unicode_names2 = { version = "1.2.1" }
ureq = { version = "2.9.6" } ureq = { version = "2.9.1" }
url = { version = "2.5.0" } url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] } uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" } walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" } wasm-bindgen = { version = "0.2.84" }
wasm-bindgen-test = { version = "0.3.40" } wasm-bindgen-test = { version = "0.3.40" }
wild = { version = "2" } wild = { version = "2" }

View File

@@ -7,9 +7,8 @@
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff) [![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff) [![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions) [![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/astral-sh)
[**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/) [**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter and code formatter, written in Rust. An extremely fast Python linter and code formatter, written in Rust.
@@ -129,7 +128,7 @@ and with [a variety of other package managers](https://docs.astral.sh/ruff/insta
To run Ruff as a linter, try any of the following: To run Ruff as a linter, try any of the following:
```shell ```shell
ruff check # Lint all files in the current directory (and any subdirectories). ruff check . # Lint all files in the current directory (and any subdirectories).
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories). ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories).
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`. ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`.
ruff check path/to/code/to/file.py # Lint `file.py`. ruff check path/to/code/to/file.py # Lint `file.py`.
@@ -139,7 +138,7 @@ ruff check @arguments.txt # Lint using an input file, treating its con
Or, to run Ruff as a formatter: Or, to run Ruff as a formatter:
```shell ```shell
ruff format # Format all files in the current directory (and any subdirectories). ruff format . # Format all files in the current directory (and any subdirectories).
ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories). ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories).
ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`. ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`.
ruff format path/to/code/to/file.py # Format `file.py`. ruff format path/to/code/to/file.py # Format `file.py`.
@@ -151,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.3.2 rev: v0.1.15
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -173,7 +172,7 @@ jobs:
ruff: ruff:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1 - uses: chartboost/ruff-action@v1
``` ```
@@ -183,9 +182,10 @@ Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml`
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/) [_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
for a complete list of all configuration options). for a complete list of all configuration options).
If left unspecified, Ruff's default configuration is equivalent to the following `ruff.toml` file: If left unspecified, Ruff's default configuration is equivalent to:
```toml ```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories. # Exclude a variety of commonly ignored directories.
exclude = [ exclude = [
".bzr", ".bzr",
@@ -223,7 +223,7 @@ indent-width = 4
# Assume Python 3.8 # Assume Python 3.8
target-version = "py38" target-version = "py38"
[lint] [tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"] select = ["E4", "E7", "E9", "F"]
ignore = [] ignore = []
@@ -235,7 +235,7 @@ unfixable = []
# Allow unused variables when underscore-prefixed. # Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format] [tool.ruff.format]
# Like Black, use double quotes for strings. # Like Black, use double quotes for strings.
quote-style = "double" quote-style = "double"
@@ -249,20 +249,11 @@ skip-magic-trailing-comma = false
line-ending = "auto" line-ending = "auto"
``` ```
Note that, in a `pyproject.toml`, each section header should be prefixed with `tool.ruff`. For Some configuration options can be provided via the command-line, such as those related to
example, `[lint]` should be replaced with `[tool.ruff.lint]`. rule enablement and disablement, file discovery, and logging level:
Some configuration options can be provided via dedicated command-line arguments, such as those
related to rule enablement and disablement, file discovery, and logging level:
```shell ```shell
ruff check --select F401 --select F403 --quiet ruff check path/to/code/ --select F401 --select F403 --quiet
```
The remaining configuration options can be provided through a catch-all `--config` argument:
```shell
ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
``` ```
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format` See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
@@ -350,14 +341,14 @@ For a complete enumeration of the supported rules, see [_Rules_](https://docs.as
Contributions are welcome and highly appreciated. To get started, check out the Contributions are welcome and highly appreciated. To get started, check out the
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/). [**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
You can also join us on [**Discord**](https://discord.com/invite/astral-sh). You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Support ## Support
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues), Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new). or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh). You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Acknowledgements ## Acknowledgements
@@ -387,7 +378,6 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including: Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model)) - Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python)) - Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow) - [Apache Airflow](https://github.com/apache/airflow)
@@ -412,9 +402,7 @@ Ruff is used by a number of major open-source projects and companies, including:
[Diffusers](https://github.com/huggingface/diffusers)) [Diffusers](https://github.com/huggingface/diffusers))
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus)) - ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
- [Ibis](https://github.com/ibis-project/ibis) - [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [Jupyter](https://github.com/jupyter-server/jupyter_server) - [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain) - [LangChain](https://github.com/hwchase17/langchain)
- [Litestar](https://litestar.dev/) - [Litestar](https://litestar.dev/)
- [LlamaIndex](https://github.com/jerryjliu/llama_index) - [LlamaIndex](https://github.com/jerryjliu/llama_index)
@@ -444,7 +432,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PyInstaller](https://github.com/pyinstaller/pyinstaller) - [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/) - [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing) - [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [pytest](https://github.com/pytest-dev/pytest)
- [PyTorch](https://github.com/pytorch/pytorch) - [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic) - [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint) - [Pylint](https://github.com/PyCQA/pylint)
@@ -475,7 +462,7 @@ Ruff is used by a number of major open-source projects and companies, including:
### Show Your Support ### Show Your Support
If you're using Ruff, consider adding the Ruff badge to your project's `README.md`: If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
```md ```md
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -501,6 +488,6 @@ MIT
<div align="center"> <div align="center">
<a target="_blank" href="https://astral.sh" style="background:none"> <a target="_blank" href="https://astral.sh" style="background:none">
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg" alt="Made by Astral"> <img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg">
</a> </a>
</div> </div>

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ruff" name = "ruff"
version = "0.3.2" version = "0.1.15"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@@ -20,7 +20,6 @@ ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" } ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" } ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" } ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" } ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" } ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" } ruff_workspace = { path = "../ruff_workspace" }
@@ -49,18 +48,12 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
shellexpand = { workspace = true } shellexpand = { workspace = true }
strum = { workspace = true, features = [] } strum = { workspace = true, features = [] }
tempfile = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true, features = ["log"] } tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["registry"]}
tracing-tree = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
wild = { workspace = true } wild = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
assert_cmd = { workspace = true } assert_cmd = { workspace = true }
# Avoid writing colored snapshots when running tests from the terminal # Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"]} colored = { workspace = true, features = ["no-color"]}

View File

@@ -1,18 +1,8 @@
use std::cmp::Ordering; use std::path::PathBuf;
use std::fmt::Formatter;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use anyhow::bail;
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser}; use clap::{command, Parser};
use colored::Colorize;
use path_absolutize::path_dedot;
use regex::Regex; use regex::Regex;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use toml;
use ruff_linter::line_width::LineLength; use ruff_linter::line_width::LineLength;
use ruff_linter::logging::LogLevel; use ruff_linter::logging::LogLevel;
@@ -22,58 +12,10 @@ use ruff_linter::settings::types::{
SerializationFormat, UnsafeFixes, SerializationFormat, UnsafeFixes,
}; };
use ruff_linter::{warn_user, 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::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions}; use ruff_workspace::options::PycodestyleOptions;
use ruff_workspace::resolver::ConfigurationTransformer; use ruff_workspace::resolver::ConfigurationTransformer;
/// All configuration options that can be passed "globally",
/// i.e., can be passed to all subcommands
#[derive(Debug, Default, Clone, clap::Args)]
pub struct GlobalConfigArgs {
#[clap(flatten)]
log_level_args: LogLevelArgs,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
global = true,
help_heading = "Global options",
)]
pub config: Vec<SingleConfigArgument>,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, help_heading = "Global options", global = true)]
pub isolated: bool,
}
impl GlobalConfigArgs {
pub fn log_level(&self) -> LogLevel {
LogLevel::from(&self.log_level_args)
}
#[must_use]
fn partition(self) -> (LogLevel, Vec<SingleConfigArgument>, bool) {
(self.log_level(), self.config, self.isolated)
}
}
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command( #[command(
author, author,
@@ -84,9 +26,9 @@ impl GlobalConfigArgs {
#[command(version)] #[command(version)]
pub struct Args { pub struct Args {
#[command(subcommand)] #[command(subcommand)]
pub(crate) command: Command, pub command: Command,
#[clap(flatten)] #[clap(flatten)]
pub(crate) global_options: GlobalConfigArgs, pub log_level_args: LogLevelArgs,
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@@ -109,6 +51,10 @@ pub enum Command {
/// Output format /// Output format
#[arg(long, value_enum, default_value = "text")] #[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat, output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
}, },
/// List or describe the available configuration options. /// List or describe the available configuration options.
Config { option: Option<String> }, Config { option: Option<String> },
@@ -117,6 +63,10 @@ pub enum Command {
/// Output format /// Output format
#[arg(long, value_enum, default_value = "text")] #[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat, output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
}, },
/// Clear any caches in the current directory and any subdirectories. /// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")] #[clap(alias = "--clean")]
@@ -126,8 +76,6 @@ pub enum Command {
GenerateShellCompletion { shell: clap_complete_command::Shell }, GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories. /// Run the Ruff formatter on the given files or directories.
Format(FormatCommand), Format(FormatCommand),
/// Run the language server.
Server(ServerCommand),
/// Display Ruff's version /// Display Ruff's version
Version { Version {
#[arg(long, value_enum, default_value = "text")] #[arg(long, value_enum, default_value = "text")]
@@ -201,6 +149,10 @@ pub struct CheckCommand {
preview: bool, preview: bool,
#[clap(long, overrides_with("preview"), hide = true)] #[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool, no_preview: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all rules). /// Comma-separated list of rule codes to enable (or ALL, to enable all rules).
#[arg( #[arg(
long, long,
@@ -332,6 +284,9 @@ pub struct CheckCommand {
/// Disable cache reads. /// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
pub no_cache: bool, pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// Path to the cache directory. /// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>, pub cache_dir: Option<PathBuf>,
@@ -423,6 +378,9 @@ pub struct FormatCommand {
/// difference between the current file and how the formatted file would look like. /// difference between the current file and how the formatted file would look like.
#[arg(long)] #[arg(long)]
pub diff: bool, pub diff: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Disable cache reads. /// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
@@ -463,6 +421,9 @@ pub struct FormatCommand {
/// Set the line-length. /// Set the line-length.
#[arg(long, help_heading = "Format configuration")] #[arg(long, help_heading = "Format configuration")]
pub line_length: Option<LineLength>, pub line_length: Option<LineLength>,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// The name of the file when passing it through stdin. /// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")] #[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>, pub stdin_filename: Option<PathBuf>,
@@ -479,28 +440,6 @@ pub struct FormatCommand {
preview: bool, preview: bool,
#[clap(long, overrides_with("preview"), hide = true)] #[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool, 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(Clone, Debug, clap::Parser)]
pub struct ServerCommand {
/// Enable preview mode; required for regular operation
#[arg(long)]
pub(crate) preview: bool,
} }
#[derive(Debug, Clone, Copy, clap::ValueEnum)] #[derive(Debug, Clone, Copy, clap::ValueEnum)]
@@ -510,7 +449,7 @@ pub enum HelpFormat {
} }
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, clap::Args)] #[derive(Debug, clap::Args)]
pub struct LogLevelArgs { pub struct LogLevelArgs {
/// Enable verbose logging. /// Enable verbose logging.
#[arg( #[arg(
@@ -555,191 +494,100 @@ impl From<&LogLevelArgs> for LogLevel {
} }
} }
/// Configuration-related arguments passed via the CLI.
#[derive(Default)]
pub struct ConfigArguments {
/// Whether the user specified --isolated on the command line
pub(crate) isolated: bool,
/// The logging level to be used, derived from command-line arguments passed
pub(crate) log_level: LogLevel,
/// Path to a pyproject.toml or ruff.toml configuration file (etc.).
/// Either 0 or 1 configuration file paths may be provided on the command line.
config_file: Option<PathBuf>,
/// Overrides provided via the `--config "KEY=VALUE"` option.
/// An arbitrary number of these overrides may be provided on the command line.
/// These overrides take precedence over all configuration files,
/// even configuration files that were also specified using `--config`.
overrides: Configuration,
/// Overrides provided via dedicated flags such as `--line-length` etc.
/// These overrides take precedence over all configuration files,
/// and also over all overrides specified using any `--config "KEY=VALUE"` flags.
per_flag_overrides: ExplicitConfigOverrides,
}
impl ConfigArguments {
pub fn config_file(&self) -> Option<&Path> {
self.config_file.as_deref()
}
fn from_cli_arguments(
global_options: GlobalConfigArgs,
per_flag_overrides: ExplicitConfigOverrides,
) -> anyhow::Result<Self> {
let (log_level, config_options, isolated) = global_options.partition();
let mut config_file: Option<PathBuf> = None;
let mut overrides = Configuration::default();
for option in config_options {
match option {
SingleConfigArgument::SettingsOverride(overridden_option) => {
let overridden_option = Arc::try_unwrap(overridden_option)
.unwrap_or_else(|option| option.deref().clone());
overrides = overrides.combine(Configuration::from_options(
overridden_option,
None,
&path_dedot::CWD,
)?);
}
SingleConfigArgument::FilePath(path) => {
if isolated {
bail!(
"\
The argument `--config={}` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
",
path.display()
);
}
if let Some(ref config_file) = config_file {
let (first, second) = (config_file.display(), path.display());
bail!(
"\
You cannot specify more than one configuration file on the command line.
tip: remove either `--config={first}` or `--config={second}`.
For more information, try `--help`.
"
);
}
config_file = Some(path);
}
}
}
Ok(Self {
isolated,
log_level,
config_file,
overrides,
per_flag_overrides,
})
}
}
impl ConfigurationTransformer for ConfigArguments {
fn transform(&self, config: Configuration) -> Configuration {
let with_config_overrides = self.overrides.clone().combine(config);
self.per_flag_overrides.transform(with_config_overrides)
}
}
impl CheckCommand { impl CheckCommand {
/// Partition the CLI into command-line arguments and configuration /// Partition the CLI into command-line arguments and configuration
/// overrides. /// overrides.
pub fn partition( pub fn partition(self) -> (CheckArguments, CliOverrides) {
self, (
global_options: GlobalConfigArgs, CheckArguments {
) -> anyhow::Result<(CheckArguments, ConfigArguments)> { add_noqa: self.add_noqa,
let check_arguments = CheckArguments { config: self.config,
add_noqa: self.add_noqa, diff: self.diff,
diff: self.diff, ecosystem_ci: self.ecosystem_ci,
ecosystem_ci: self.ecosystem_ci, exit_non_zero_on_fix: self.exit_non_zero_on_fix,
exit_non_zero_on_fix: self.exit_non_zero_on_fix, exit_zero: self.exit_zero,
exit_zero: self.exit_zero, files: self.files,
files: self.files, ignore_noqa: self.ignore_noqa,
ignore_noqa: self.ignore_noqa, isolated: self.isolated,
no_cache: self.no_cache, no_cache: self.no_cache,
output_file: self.output_file, output_file: self.output_file,
show_files: self.show_files, show_files: self.show_files,
show_settings: self.show_settings, show_settings: self.show_settings,
statistics: self.statistics, statistics: self.statistics,
stdin_filename: self.stdin_filename, stdin_filename: self.stdin_filename,
watch: self.watch, watch: self.watch,
}; },
CliOverrides {
let cli_overrides = ExplicitConfigOverrides { dummy_variable_rgx: self.dummy_variable_rgx,
dummy_variable_rgx: self.dummy_variable_rgx, exclude: self.exclude,
exclude: self.exclude, extend_exclude: self.extend_exclude,
extend_exclude: self.extend_exclude, extend_fixable: self.extend_fixable,
extend_fixable: self.extend_fixable, extend_ignore: self.extend_ignore,
extend_ignore: self.extend_ignore, extend_per_file_ignores: self.extend_per_file_ignores,
extend_per_file_ignores: self.extend_per_file_ignores, extend_select: self.extend_select,
extend_select: self.extend_select, extend_unfixable: self.extend_unfixable,
extend_unfixable: self.extend_unfixable, fixable: self.fixable,
fixable: self.fixable, ignore: self.ignore,
ignore: self.ignore, line_length: self.line_length,
line_length: self.line_length, per_file_ignores: self.per_file_ignores,
per_file_ignores: self.per_file_ignores, preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from), respect_gitignore: resolve_bool_arg(
respect_gitignore: resolve_bool_arg(self.respect_gitignore, self.no_respect_gitignore), self.respect_gitignore,
select: self.select, self.no_respect_gitignore,
target_version: self.target_version, ),
unfixable: self.unfixable, select: self.select,
// TODO(charlie): Included in `pyproject.toml`, but not inherited. target_version: self.target_version,
cache_dir: self.cache_dir, unfixable: self.unfixable,
fix: resolve_bool_arg(self.fix, self.no_fix), // TODO(charlie): Included in `pyproject.toml`, but not inherited.
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only), cache_dir: self.cache_dir,
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes) fix: resolve_bool_arg(self.fix, self.no_fix),
.map(UnsafeFixes::from), fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
output_format: resolve_output_format( .map(UnsafeFixes::from),
self.output_format, force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
resolve_bool_arg(self.show_source, self.no_show_source), output_format: resolve_output_format(
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(), self.output_format,
), resolve_bool_arg(self.show_source, self.no_show_source),
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes), resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
extension: self.extension, ),
}; show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
extension: self.extension,
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?; },
Ok((check_arguments, config_args)) )
} }
} }
impl FormatCommand { impl FormatCommand {
/// Partition the CLI into command-line arguments and configuration /// Partition the CLI into command-line arguments and configuration
/// overrides. /// overrides.
pub fn partition( pub fn partition(self) -> (FormatArguments, CliOverrides) {
self, (
global_options: GlobalConfigArgs, FormatArguments {
) -> anyhow::Result<(FormatArguments, ConfigArguments)> { check: self.check,
let format_arguments = FormatArguments { diff: self.diff,
check: self.check, config: self.config,
diff: self.diff, files: self.files,
files: self.files, isolated: self.isolated,
no_cache: self.no_cache, no_cache: self.no_cache,
stdin_filename: self.stdin_filename, stdin_filename: self.stdin_filename,
range: self.range, },
}; CliOverrides {
line_length: self.line_length,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,
),
exclude: self.exclude,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
target_version: self.target_version,
cache_dir: self.cache_dir,
extension: self.extension,
let cli_overrides = ExplicitConfigOverrides { // Unsupported on the formatter CLI, but required on `Overrides`.
line_length: self.line_length, ..CliOverrides::default()
respect_gitignore: resolve_bool_arg(self.respect_gitignore, self.no_respect_gitignore), },
exclude: self.exclude, )
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
target_version: self.target_version,
cache_dir: self.cache_dir,
extension: self.extension,
// Unsupported on the formatter CLI, but required on `Overrides`.
..ExplicitConfigOverrides::default()
};
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
Ok((format_arguments, config_args))
} }
} }
@@ -752,180 +600,6 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
} }
} }
/// Enumeration of various ways in which a --config CLI flag
/// could be invalid
#[derive(Debug)]
enum InvalidConfigFlagReason {
InvalidToml(toml::de::Error),
/// It was valid TOML, but not a valid ruff config file.
/// E.g. the user tried to select a rule that doesn't exist,
/// or tried to enable a setting that doesn't exist
ValidTomlButInvalidRuffSchema(toml::de::Error),
/// It was a valid ruff config file, but the user tried to pass a
/// value for `extend` as part of the config override.
// `extend` is special, because it affects which config files we look at
/// in the first place. We currently only parse --config overrides *after*
/// we've combined them with all the arguments from the various config files
/// that we found, so trying to override `extend` as part of a --config
/// override is forbidden.
ExtendPassedViaConfigFlag,
}
impl InvalidConfigFlagReason {
const fn description(&self) -> &'static str {
match self {
Self::InvalidToml(_) => "The supplied argument is not valid TOML",
Self::ValidTomlButInvalidRuffSchema(_) => {
"Could not parse the supplied argument as a `ruff.toml` configuration option"
}
Self::ExtendPassedViaConfigFlag => "Cannot include `extend` in a --config flag value",
}
}
}
/// Enumeration to represent a single `--config` argument
/// passed via the CLI.
///
/// Using the `--config` flag, users may pass 0 or 1 paths
/// to configuration files and an arbitrary number of
/// "inline TOML" overrides for specific settings.
///
/// For example:
///
/// ```sh
/// ruff check --config "path/to/ruff.toml" --config "extend-select=['E501', 'F841']" --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
/// ```
#[derive(Clone, Debug)]
pub enum SingleConfigArgument {
FilePath(PathBuf),
SettingsOverride(Arc<Options>),
}
#[derive(Clone)]
pub struct ConfigArgumentParser;
impl ValueParserFactory for SingleConfigArgument {
type Parser = ConfigArgumentParser;
fn value_parser() -> Self::Parser {
ConfigArgumentParser
}
}
impl TypedValueParser for ConfigArgumentParser {
type Value = SingleConfigArgument;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
// Convert to UTF-8.
let Some(value) = value.to_str() else {
// But respect non-UTF-8 paths.
let path_to_config_file = PathBuf::from(value);
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
return Err(clap::Error::new(clap::error::ErrorKind::InvalidUtf8));
};
// Expand environment variables and tildes.
if let Ok(path_to_config_file) =
shellexpand::full(value).map(|config| PathBuf::from(&*config))
{
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
}
let config_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into::<Options>() {
Ok(option) => {
if option.extend.is_none() {
return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option)));
}
InvalidConfigFlagReason::ExtendPassedViaConfigFlag
}
Err(underlying_error) => {
InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error)
}
},
Err(underlying_error) => InvalidConfigFlagReason::InvalidToml(underlying_error),
};
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
new_error.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
new_error.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
let underlying_error = match &config_parse_error {
InvalidConfigFlagReason::ExtendPassedViaConfigFlag => {
let tip = config_parse_error.description().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
return Err(new_error);
}
InvalidConfigFlagReason::InvalidToml(underlying_error)
| InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error) => {
underlying_error
}
};
// small hack so that multiline tips
// have the same indent on the left-hand side:
let tip_indent = " ".repeat(" tip: ".len());
let mut tip = format!(
"\
A `--config` flag must either be a path to a `.toml` configuration file
{tip_indent}or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
{tip_indent}option"
);
// Here we do some heuristics to try to figure out whether
// the user was trying to pass in a path to a configuration file
// or some inline TOML.
// We want to display the most helpful error to the user as possible.
if std::path::Path::new(value)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
{
if !value.contains('=') {
tip.push_str(&format!(
"
It looks like you were trying to pass a path to a configuration file.
The path `{value}` does not point to a configuration file"
));
}
} else if value.contains('=') {
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
}
let tip = tip.trim_end().to_owned().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
Err(new_error)
}
}
fn resolve_output_format( fn resolve_output_format(
output_format: Option<SerializationFormat>, output_format: Option<SerializationFormat>,
show_sources: Option<bool>, show_sources: Option<bool>,
@@ -968,12 +642,14 @@ fn resolve_output_format(
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct CheckArguments { pub struct CheckArguments {
pub add_noqa: bool, pub add_noqa: bool,
pub config: Option<PathBuf>,
pub diff: bool, pub diff: bool,
pub ecosystem_ci: bool, pub ecosystem_ci: bool,
pub exit_non_zero_on_fix: bool, pub exit_non_zero_on_fix: bool,
pub exit_zero: bool, pub exit_zero: bool,
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
pub ignore_noqa: bool, pub ignore_noqa: bool,
pub isolated: bool,
pub no_cache: bool, pub no_cache: bool,
pub output_file: Option<PathBuf>, pub output_file: Option<PathBuf>,
pub show_files: bool, pub show_files: bool,
@@ -990,234 +666,45 @@ pub struct FormatArguments {
pub check: bool, pub check: bool,
pub no_cache: bool, pub no_cache: bool,
pub diff: bool, pub diff: bool,
pub config: Option<PathBuf>,
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>, pub stdin_filename: Option<PathBuf>,
pub range: Option<FormatRange>,
} }
/// A text range specified by line and column numbers. /// CLI settings that function as configuration overrides.
#[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()
)
}
}
}
}
/// Configuration overrides provided via dedicated CLI flags:
/// `--line-length`, `--respect-gitignore`, etc.
#[derive(Clone, Default)] #[derive(Clone, Default)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
struct ExplicitConfigOverrides { pub struct CliOverrides {
dummy_variable_rgx: Option<Regex>, pub dummy_variable_rgx: Option<Regex>,
exclude: Option<Vec<FilePattern>>, pub exclude: Option<Vec<FilePattern>>,
extend_exclude: Option<Vec<FilePattern>>, pub extend_exclude: Option<Vec<FilePattern>>,
extend_fixable: Option<Vec<RuleSelector>>, pub extend_fixable: Option<Vec<RuleSelector>>,
extend_ignore: Option<Vec<RuleSelector>>, pub extend_ignore: Option<Vec<RuleSelector>>,
extend_select: Option<Vec<RuleSelector>>, pub extend_select: Option<Vec<RuleSelector>>,
extend_unfixable: Option<Vec<RuleSelector>>, pub extend_unfixable: Option<Vec<RuleSelector>>,
fixable: Option<Vec<RuleSelector>>, pub fixable: Option<Vec<RuleSelector>>,
ignore: Option<Vec<RuleSelector>>, pub ignore: Option<Vec<RuleSelector>>,
line_length: Option<LineLength>, pub line_length: Option<LineLength>,
per_file_ignores: Option<Vec<PatternPrefixPair>>, pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
extend_per_file_ignores: Option<Vec<PatternPrefixPair>>, pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
preview: Option<PreviewMode>, pub preview: Option<PreviewMode>,
respect_gitignore: Option<bool>, pub respect_gitignore: Option<bool>,
select: Option<Vec<RuleSelector>>, pub select: Option<Vec<RuleSelector>>,
target_version: Option<PythonVersion>, pub target_version: Option<PythonVersion>,
unfixable: Option<Vec<RuleSelector>>, pub unfixable: Option<Vec<RuleSelector>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`. // TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
cache_dir: Option<PathBuf>, pub cache_dir: Option<PathBuf>,
fix: Option<bool>, pub fix: Option<bool>,
fix_only: Option<bool>, pub fix_only: Option<bool>,
unsafe_fixes: Option<UnsafeFixes>, pub unsafe_fixes: Option<UnsafeFixes>,
force_exclude: Option<bool>, pub force_exclude: Option<bool>,
output_format: Option<SerializationFormat>, pub output_format: Option<SerializationFormat>,
show_fixes: Option<bool>, pub show_fixes: Option<bool>,
extension: Option<Vec<ExtensionPair>>, pub extension: Option<Vec<ExtensionPair>>,
} }
impl ConfigurationTransformer for ExplicitConfigOverrides { impl ConfigurationTransformer for CliOverrides {
fn transform(&self, mut config: Configuration) -> Configuration { fn transform(&self, mut config: Configuration) -> Configuration {
if let Some(cache_dir) = &self.cache_dir { if let Some(cache_dir) = &self.cache_dir {
config.cache_dir = Some(cache_dir.clone()); config.cache_dir = Some(cache_dir.clone());

View File

@@ -1,7 +1,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::{self, File}; use std::fs::{self, File};
use std::hash::Hasher; use std::hash::Hasher;
use std::io::{self, BufReader, Write}; use std::io::{self, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
@@ -15,7 +15,6 @@ use rayon::iter::ParallelIterator;
use rayon::iter::{IntoParallelIterator, ParallelBridge}; use rayon::iter::{IntoParallelIterator, ParallelBridge};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_diagnostics::{DiagnosticKind, Fix};
@@ -166,29 +165,15 @@ impl Cache {
return Ok(()); return Ok(());
} }
// Write the cache to a temporary file first and then rename it for an "atomic" write. let file = File::create(&self.path)
// Protects against data loss if the process is killed during the write and races between different ruff .with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?;
// processes, resulting in a corrupted cache file. https://github.com/astral-sh/ruff/issues/8147#issuecomment-1943345964 let writer = BufWriter::new(file);
let mut temp_file = bincode::serialize_into(writer, &self.package).with_context(|| {
NamedTempFile::new_in(self.path.parent().expect("Write path must have a parent"))
.context("Failed to create temporary file")?;
// Serialize to in-memory buffer because hyperfine benchmark showed that it's faster than
// using a `BufWriter` and our cache files are small enough that streaming isn't necessary.
let serialized =
bincode::serialize(&self.package).context("Failed to serialize cache data")?;
temp_file
.write_all(&serialized)
.context("Failed to write serialized cache to temporary file.")?;
temp_file.persist(&self.path).with_context(|| {
format!( format!(
"Failed to rename temporary cache file to {}", "Failed to serialise cache to file '{}'",
self.path.display() self.path.display()
) )
})?; })
Ok(())
} }
/// Applies the pending changes without storing the cache to disk. /// Applies the pending changes without storing the cache to disk.
@@ -383,7 +368,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
let gitignore_path = path.join(".gitignore"); let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() { if !gitignore_path.exists() {
let mut file = fs::File::create(gitignore_path)?; let mut file = fs::File::create(gitignore_path)?;
file.write_all(b"# Automatically created by ruff.\n*\n")?; file.write_all(b"*")?;
} }
Ok(()) Ok(())
@@ -1065,7 +1050,6 @@ mod tests {
&self.settings.formatter, &self.settings.formatter,
PySourceType::Python, PySourceType::Python,
FormatMode::Write, FormatMode::Write,
None,
Some(cache), Some(cache),
) )
} }

View File

@@ -12,17 +12,17 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Add `noqa` directives to a collection of files. /// Add `noqa` directives to a collection of files.
pub(crate) fn add_noqa( pub(crate) fn add_noqa(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
) -> Result<usize> { ) -> Result<usize> {
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);

View File

@@ -24,7 +24,7 @@ use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile,
}; };
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use crate::cache::{Cache, PackageCacheMap, PackageCaches}; use crate::cache::{Cache, PackageCacheMap, PackageCaches};
use crate::diagnostics::Diagnostics; use crate::diagnostics::Diagnostics;
use crate::panic::catch_unwind; use crate::panic::catch_unwind;
@@ -34,7 +34,7 @@ use crate::panic::catch_unwind;
pub(crate) fn check( pub(crate) fn check(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
cache: flags::Cache, cache: flags::Cache,
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
@@ -42,7 +42,7 @@ pub(crate) fn check(
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
// Collect all the Python files to check. // Collect all the Python files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
debug!("Identified files to lint in: {:?}", start.elapsed()); debug!("Identified files to lint in: {:?}", start.elapsed());
if paths.is_empty() { if paths.is_empty() {
@@ -233,7 +233,7 @@ mod test {
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use super::check; use super::check;
@@ -272,7 +272,7 @@ mod test {
// Notebooks are not included by default // Notebooks are not included by default
&[tempdir.path().to_path_buf(), notebook], &[tempdir.path().to_path_buf(), notebook],
&pyproject_config, &pyproject_config,
&ConfigArguments::default(), &CliOverrides::default(),
flags::Cache::Disabled, flags::Cache::Disabled,
flags::Noqa::Disabled, flags::Noqa::Disabled,
flags::FixMode::Generate, flags::FixMode::Generate,

View File

@@ -6,7 +6,7 @@ use ruff_linter::packaging;
use ruff_linter::settings::flags; use ruff_linter::settings::flags;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver}; use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics}; use crate::diagnostics::{lint_stdin, Diagnostics};
use crate::stdin::{parrot_stdin, read_from_stdin}; use crate::stdin::{parrot_stdin, read_from_stdin};
@@ -14,7 +14,7 @@ use crate::stdin::{parrot_stdin, read_from_stdin};
pub(crate) fn check_stdin( pub(crate) fn check_stdin(
filename: Option<&Path>, filename: Option<&Path>,
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
overrides: &ConfigArguments, overrides: &CliOverrides,
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {

View File

@@ -23,13 +23,12 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::warn_user_once; use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle}; use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_source_file::LineIndex;
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver}; use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
use ruff_workspace::FormatterSettings; use ruff_workspace::FormatterSettings;
use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::args::{CliOverrides, FormatArguments};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches}; use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
use crate::panic::{catch_unwind, PanicError}; use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve; use crate::resolve::resolve;
@@ -60,25 +59,24 @@ impl FormatMode {
/// Format a set of files, and return the exit status. /// Format a set of files, and return the exit status.
pub(crate) fn format( pub(crate) fn format(
cli: FormatArguments, cli: FormatArguments,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
log_level: LogLevel,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?; let pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
overrides,
cli.stdin_filename.as_deref(),
)?;
let mode = FormatMode::from_cli(&cli); let mode = FormatMode::from_cli(&cli);
let files = resolve_default_files(cli.files, false); let files = resolve_default_files(cli.files, false);
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(&files, &pyproject_config, overrides)?;
if paths.is_empty() { if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)"); warn_user_once!("No Python files found under the given path(s)");
return Ok(ExitStatus::Success); 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); warn_incompatible_formatter_settings(&resolver);
// Discover the package root for each Python file. // Discover the package root for each Python file.
@@ -141,14 +139,7 @@ pub(crate) fn format(
Some( Some(
match catch_unwind(|| { match catch_unwind(|| {
format_path( format_path(path, &settings.formatter, source_type, mode, cache)
path,
&settings.formatter,
source_type,
mode,
cli.range,
cache,
)
}) { }) {
Ok(inner) => inner.map(|result| FormatPathResult { Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.path().to_path_buf(), path: resolved_file.path().to_path_buf(),
@@ -197,7 +188,7 @@ pub(crate) fn format(
} }
// Report on the formatting changes. // Report on the formatting changes.
if config_arguments.log_level >= LogLevel::Default { if log_level >= LogLevel::Default {
if mode.is_diff() { if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr // Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?; results.write_summary(&mut stderr().lock())?;
@@ -235,7 +226,6 @@ pub(crate) fn format_path(
settings: &FormatterSettings, settings: &FormatterSettings,
source_type: PySourceType, source_type: PySourceType,
mode: FormatMode, mode: FormatMode,
range: Option<FormatRange>,
cache: Option<&Cache>, cache: Option<&Cache>,
) -> Result<FormatResult, FormatCommandError> { ) -> Result<FormatResult, FormatCommandError> {
if let Some(cache) = cache { if let Some(cache) = cache {
@@ -260,12 +250,8 @@ 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. // Format the source.
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)? let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
{
FormattedSource::Formatted(formatted) => match mode { FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => { FormatMode::Write => {
let mut writer = File::create(path).map_err(|err| { let mut writer = File::create(path).map_err(|err| {
@@ -333,31 +319,12 @@ pub(crate) fn format_source(
source_type: PySourceType, source_type: PySourceType,
path: Option<&Path>, path: Option<&Path>,
settings: &FormatterSettings, settings: &FormatterSettings,
range: Option<FormatRange>,
) -> Result<FormattedSource, FormatCommandError> { ) -> Result<FormattedSource, FormatCommandError> {
match &source_kind { match &source_kind {
SourceKind::Python(unformatted) => { SourceKind::Python(unformatted) => {
let options = settings.to_format_options(source_type, unformatted); let options = settings.to_format_options(source_type, unformatted);
let formatted = if let Some(range) = range { let formatted = format_module_source(unformatted, options).map_err(|err| {
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 { if let FormatModuleError::ParseError(err) = err {
DisplayParseError::from_source_kind( DisplayParseError::from_source_kind(
err, err,
@@ -370,6 +337,7 @@ pub(crate) fn format_source(
} }
})?; })?;
let formatted = formatted.into_code();
if formatted.len() == unformatted.len() && formatted == *unformatted { if formatted.len() == unformatted.len() && formatted == *unformatted {
Ok(FormattedSource::Unchanged) Ok(FormattedSource::Unchanged)
} else { } else {
@@ -381,12 +349,6 @@ pub(crate) fn format_source(
return Ok(FormattedSource::Unchanged); 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 options = settings.to_format_options(source_type, notebook.source_code());
let mut output: Option<String> = None; let mut output: Option<String> = None;
@@ -527,7 +489,7 @@ impl<'a> FormatResults<'a> {
}) })
.sorted_unstable_by_key(|(path, _, _)| *path) .sorted_unstable_by_key(|(path, _, _)| *path)
{ {
write!(f, "{}", unformatted.diff(formatted, Some(path)).unwrap())?; unformatted.diff(formatted, Some(path), f)?;
} }
Ok(()) Ok(())
@@ -627,7 +589,6 @@ pub(crate) enum FormatCommandError {
Format(Option<PathBuf>, FormatModuleError), Format(Option<PathBuf>, FormatModuleError),
Write(Option<PathBuf>, SourceError), Write(Option<PathBuf>, SourceError),
Diff(Option<PathBuf>, io::Error), Diff(Option<PathBuf>, io::Error),
RangeFormatNotebook(Option<PathBuf>),
} }
impl FormatCommandError { impl FormatCommandError {
@@ -645,8 +606,7 @@ impl FormatCommandError {
| Self::Read(path, _) | Self::Read(path, _)
| Self::Format(path, _) | Self::Format(path, _)
| Self::Write(path, _) | Self::Write(path, _)
| Self::Diff(path, _) | Self::Diff(path, _) => path.as_deref(),
| Self::RangeFormatNotebook(path) => path.as_deref(),
} }
} }
} }
@@ -668,10 +628,9 @@ impl Display for FormatCommandError {
} else { } else {
write!( write!(
f, f,
"{header} {error}", "{} {}",
header = "Encountered error:".bold(), "Encountered error:".bold(),
error = err err.io_error()
.io_error()
.map_or_else(|| err.to_string(), std::string::ToString::to_string) .map_or_else(|| err.to_string(), std::string::ToString::to_string)
) )
} }
@@ -689,7 +648,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to read:".bold()) write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
} }
} }
Self::Write(path, err) => { Self::Write(path, err) => {
@@ -702,7 +661,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to write:".bold()) write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
} }
} }
Self::Format(path, err) => { Self::Format(path, err) => {
@@ -715,7 +674,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to format:".bold()) write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
} }
} }
Self::Diff(path, err) => { Self::Diff(path, err) => {
@@ -730,25 +689,9 @@ impl Display for FormatCommandError {
} else { } else {
write!( write!(
f, f,
"{header} {err}", "{}{} {err}",
header = "Failed to generate diff:".bold(), "Failed to generate diff".bold(),
) ":".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()
) )
} }
} }

View File

@@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver}; use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
use ruff_workspace::FormatterSettings; use ruff_workspace::FormatterSettings;
use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{ use crate::commands::format::{
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode, format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
FormatResult, FormattedSource, FormatResult, FormattedSource,
@@ -19,11 +19,13 @@ use crate::stdin::{parrot_stdin, read_from_stdin};
use crate::ExitStatus; use crate::ExitStatus;
/// Run the formatter over a single file, read from `stdin`. /// Run the formatter over a single file, read from `stdin`.
pub(crate) fn format_stdin( pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> Result<ExitStatus> {
cli: &FormatArguments, let pyproject_config = resolve(
config_arguments: &ConfigArguments, cli.isolated,
) -> Result<ExitStatus> { cli.config.as_deref(),
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?; overrides,
cli.stdin_filename.as_deref(),
)?;
let mut resolver = Resolver::new(&pyproject_config); let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver); warn_incompatible_formatter_settings(&resolver);
@@ -32,7 +34,7 @@ pub(crate) fn format_stdin(
if resolver.force_exclude() { if resolver.force_exclude() {
if let Some(filename) = cli.stdin_filename.as_deref() { if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &mut resolver, config_arguments)? { if !python_file_at_path(filename, &mut resolver, overrides)? {
if mode.is_write() { if mode.is_write() {
parrot_stdin()?; parrot_stdin()?;
} }
@@ -67,7 +69,7 @@ pub(crate) fn format_stdin(
}; };
// Format the file. // Format the file.
match format_source_code(path, cli.range, settings, source_type, mode) { match format_source_code(path, settings, source_type, mode) {
Ok(result) => match mode { Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success), FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => { FormatMode::Check | FormatMode::Diff => {
@@ -88,7 +90,6 @@ pub(crate) fn format_stdin(
/// Format source code read from `stdin`. /// Format source code read from `stdin`.
fn format_source_code( fn format_source_code(
path: Option<&Path>, path: Option<&Path>,
range: Option<FormatRange>,
settings: &FormatterSettings, settings: &FormatterSettings,
source_type: PySourceType, source_type: PySourceType,
mode: FormatMode, mode: FormatMode,
@@ -106,7 +107,7 @@ fn format_source_code(
}; };
// Format the source. // Format the source.
let formatted = format_source(&source_kind, source_type, path, settings, range)?; let formatted = format_source(&source_kind, source_type, path, settings)?;
match &formatted { match &formatted {
FormattedSource::Formatted(formatted) => match mode { FormattedSource::Formatted(formatted) => match mode {
@@ -118,13 +119,9 @@ fn format_source_code(
} }
FormatMode::Check => {} FormatMode::Check => {}
FormatMode::Diff => { FormatMode::Diff => {
use std::io::Write; source_kind
write!( .diff(formatted, path, &mut stdout().lock())
&mut stdout().lock(), .map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
"{}",
source_kind.diff(formatted, path).unwrap()
)
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
} }
}, },
FormattedSource::Unchanged => { FormattedSource::Unchanged => {

View File

@@ -7,7 +7,6 @@ pub(crate) mod format;
pub(crate) mod format_stdin; pub(crate) mod format_stdin;
pub(crate) mod linter; pub(crate) mod linter;
pub(crate) mod rule; pub(crate) mod rule;
pub(crate) mod server;
pub(crate) mod show_files; pub(crate) mod show_files;
pub(crate) mod show_settings; pub(crate) mod show_settings;
pub(crate) mod version; pub(crate) mod version;

View File

@@ -1,73 +0,0 @@
use crate::ExitStatus;
use anyhow::Result;
use ruff_linter::logging::LogLevel;
use ruff_server::Server;
use tracing::{level_filters::LevelFilter, metadata::Level, subscriber::Interest, Metadata};
use tracing_subscriber::{
layer::{Context, Filter, SubscriberExt},
Layer, Registry,
};
use tracing_tree::time::Uptime;
pub(crate) fn run_server(preview: bool, log_level: LogLevel) -> Result<ExitStatus> {
if !preview {
tracing::error!("--preview needs to be provided as a command line argument while the server is still unstable.\nFor example: `ruff server --preview`");
return Ok(ExitStatus::Error);
}
let trace_level = if log_level == LogLevel::Verbose {
Level::TRACE
} else {
Level::DEBUG
};
let subscriber = Registry::default().with(
tracing_tree::HierarchicalLayer::default()
.with_indent_lines(true)
.with_indent_amount(2)
.with_bracketed_fields(true)
.with_targets(true)
.with_writer(|| Box::new(std::io::stderr()))
.with_timer(Uptime::default())
.with_filter(LoggingFilter { trace_level }),
);
tracing::subscriber::set_global_default(subscriber)?;
let server = Server::new()?;
server.run().map(|()| ExitStatus::Success)
}
struct LoggingFilter {
trace_level: Level,
}
impl LoggingFilter {
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
let filter = if meta.target().starts_with("ruff") {
self.trace_level
} else {
Level::INFO
};
meta.level() <= &filter
}
}
impl<S> Filter<S> for LoggingFilter {
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
self.is_enabled(meta)
}
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
if self.is_enabled(meta) {
Interest::always()
} else {
Interest::never()
}
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(LevelFilter::from_level(self.trace_level))
}
}

View File

@@ -7,17 +7,17 @@ use itertools::Itertools;
use ruff_linter::warn_user_once; use ruff_linter::warn_user_once;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Show the list of files to be checked based on current settings. /// Show the list of files to be checked based on current settings.
pub(crate) fn show_files( pub(crate) fn show_files(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
writer: &mut impl Write, writer: &mut impl Write,
) -> Result<()> { ) -> Result<()> {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, _resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, _resolver) = python_files_in_path(files, pyproject_config, overrides)?;
if paths.is_empty() { if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)"); warn_user_once!("No Python files found under the given path(s)");

View File

@@ -6,17 +6,17 @@ use itertools::Itertools;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Print the user-facing configuration settings. /// Print the user-facing configuration settings.
pub(crate) fn show_settings( pub(crate) fn show_settings(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
writer: &mut impl Write, writer: &mut impl Write,
) -> Result<()> { ) -> Result<()> {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
// Print the list of files. // Print the list of files.
let Some(path) = paths let Some(path) = paths
@@ -31,9 +31,9 @@ pub(crate) fn show_settings(
let settings = resolver.resolve(&path); let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?; writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() { if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?; writeln!(writer, "Settings path: {settings_path:?}")?;
} }
write!(writer, "{settings}")?; write!(writer, "{settings}")?;

View File

@@ -3,7 +3,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::io::Write;
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign};
use std::path::Path; use std::path::Path;
@@ -290,10 +289,10 @@ pub(crate) fn lint_path(
match fix_mode { match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?, flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
flags::FixMode::Diff => { flags::FixMode::Diff => {
write!( source_kind.diff(
transformed.as_ref(),
Some(path),
&mut io::stdout().lock(), &mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, Some(path)).unwrap()
)?; )?;
} }
flags::FixMode::Generate => {} flags::FixMode::Generate => {}
@@ -443,11 +442,7 @@ pub(crate) fn lint_stdin(
flags::FixMode::Diff => { flags::FixMode::Diff => {
// But only write a diff if it's non-empty. // But only write a diff if it's non-empty.
if !fixed.is_empty() { if !fixed.is_empty() {
write!( source_kind.diff(transformed.as_ref(), path, &mut io::stdout().lock())?;
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
} }
} }
flags::FixMode::Generate => {} flags::FixMode::Generate => {}

View File

@@ -7,7 +7,6 @@ use std::process::ExitCode;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use anyhow::Result; use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory; use clap::CommandFactory;
use colored::Colorize; use colored::Colorize;
use log::warn; use log::warn;
@@ -19,7 +18,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once}; use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::args::{Args, CheckCommand, Command, FormatCommand}; use crate::args::{Args, CheckCommand, Command, FormatCommand, HelpFormat};
use crate::printer::{Flags as PrinterFlags, Printer}; use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args; pub mod args;
@@ -115,12 +114,20 @@ fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
} }
} }
/// Get the actual value of the `format` desired from either `output_format`
/// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if format.is_some() {
warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
}
format.unwrap_or(output_format)
}
pub fn run( pub fn run(
Args { Args {
command, command,
global_options, log_level_args,
}: Args, }: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
{ {
let default_panic_hook = std::panic::take_hook(); let default_panic_hook = std::panic::take_hook();
@@ -148,11 +155,8 @@ pub fn run(
#[cfg(windows)] #[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok()); assert!(colored::control::set_virtual_terminal(true).is_ok());
set_up_logging(global_options.log_level())?; let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command { match command {
Command::Version { output_format } => { Command::Version { output_format } => {
@@ -162,8 +166,10 @@ pub fn run(
Command::Rule { Command::Rule {
rule, rule,
all, all,
output_format, format,
mut output_format,
} => { } => {
output_format = resolve_help_output_format(output_format, format);
if all { if all {
commands::rule::rules(output_format)?; commands::rule::rules(output_format)?;
} }
@@ -176,46 +182,48 @@ pub fn run(
commands::config::config(option.as_deref())?; commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Linter { output_format } => { Command::Linter {
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
commands::linter::linter(output_format)?; commands::linter::linter(output_format)?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Clean => { Command::Clean => {
commands::clean::clean(global_options.log_level())?; commands::clean::clean(log_level)?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::GenerateShellCompletion { shell } => { Command::GenerateShellCompletion { shell } => {
shell.generate(&mut Args::command(), &mut stdout()); shell.generate(&mut Args::command(), &mut stdout());
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Check(args) => check(args, global_options), Command::Check(args) => check(args, log_level),
Command::Format(args) => format(args, global_options), Command::Format(args) => format(args, log_level),
Command::Server(args) => server(args, global_options.log_level()),
} }
} }
fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> { fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?; let (cli, overrides) = args.partition();
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
commands::format_stdin::format_stdin(&cli, &config_arguments) commands::format_stdin::format_stdin(&cli, &overrides)
} else { } else {
commands::format::format(cli, &config_arguments) commands::format::format(cli, &overrides, log_level)
} }
} }
#[allow(clippy::needless_pass_by_value)] // TODO: remove once we start taking arguments from here pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> { let (cli, overrides) = args.partition();
let ServerCommand { preview } = args;
commands::server::run_server(preview, log_level)
}
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
// Construct the "default" settings. These are used when no `pyproject.toml` // Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy. // files are present, or files are injected from outside of the hierarchy.
let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; let pyproject_config = resolve::resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
let mut writer: Box<dyn Write> = match cli.output_file { let mut writer: Box<dyn Write> = match cli.output_file {
Some(path) if !cli.watch => { Some(path) if !cli.watch => {
@@ -231,21 +239,11 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let files = resolve_default_files(cli.files, is_stdin); let files = resolve_default_files(cli.files, is_stdin);
if cli.show_settings { if cli.show_settings {
commands::show_settings::show_settings( commands::show_settings::show_settings(&files, &pyproject_config, &overrides, &mut writer)?;
&files,
&pyproject_config,
&config_arguments,
&mut writer,
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
if cli.show_files { if cli.show_files {
commands::show_files::show_files( commands::show_files::show_files(&files, &pyproject_config, &overrides, &mut writer)?;
&files,
&pyproject_config,
&config_arguments,
&mut writer,
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
@@ -304,9 +302,8 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
if !fix_mode.is_generate() { if !fix_mode.is_generate() {
warn_user!("--fix is incompatible with --add-noqa."); warn_user!("--fix is incompatible with --add-noqa.");
} }
let modifications = let modifications = commands::add_noqa::add_noqa(&files, &pyproject_config, &overrides)?;
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?; if modifications > 0 && log_level >= LogLevel::Default {
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" }; let s = if modifications == 1 { "" } else { "s" };
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
{ {
@@ -318,17 +315,13 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let printer = Printer::new( let printer = Printer::new(
output_format, output_format,
config_arguments.log_level, log_level,
fix_mode, fix_mode,
unsafe_fixes, unsafe_fixes,
printer_flags, printer_flags,
); );
// the settings should already be combined with the CLI overrides at this point let preview = overrides.preview.unwrap_or_default().is_enabled();
// 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 cli.watch {
if output_format != SerializationFormat::default(preview) { if output_format != SerializationFormat::default(preview) {
@@ -355,7 +348,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let messages = commands::check::check( let messages = commands::check::check(
&files, &files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,
@@ -375,8 +368,12 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
}; };
if matches!(change_kind, ChangeKind::Configuration) { if matches!(change_kind, ChangeKind::Configuration) {
pyproject_config = pyproject_config = resolve::resolve(
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
} }
Printer::clear_screen()?; Printer::clear_screen()?;
printer.write_to_user("File change detected...\n"); printer.write_to_user("File change detected...\n");
@@ -384,7 +381,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let messages = commands::check::check( let messages = commands::check::check(
&files, &files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,
@@ -401,7 +398,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
commands::check_stdin::check_stdin( commands::check_stdin::check_stdin(
cli.stdin_filename.map(fs::normalize_path).as_deref(), cli.stdin_filename.map(fs::normalize_path).as_deref(),
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
noqa.into(), noqa.into(),
fix_mode, fix_mode,
)? )?
@@ -409,7 +406,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
commands::check::check( commands::check::check(
&files, &files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,

View File

@@ -27,42 +27,26 @@ pub fn main() -> ExitCode {
let mut args = let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap(); argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// We can't use `warn_user` here because logging isn't set up at this point // Clap doesn't support default subcommands but we want to run `check` by
// and we also don't know if the user runs ruff with quiet. // default for convenience and backwards-compatibility, so we just
// Keep the message and pass it to `run` that is responsible for emitting the warning. // preprocess the arguments accordingly before passing them to Clap.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) { if let Some(arg) = args.get(1) {
// Deprecated aliases that are handled by clap if arg
Some("--explain") => { .to_str()
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.") .is_some_and(|arg| !Command::has_subcommand(rewrite_legacy_subcommand(arg)))
}
Some("--clean") => {
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
Some(arg) if !Command::has_subcommand(arg)
&& arg != "-h" && arg != "-h"
&& arg != "--help" && arg != "--help"
&& arg != "-V" && arg != "-V"
&& arg != "--version" && arg != "--version"
&& arg != "help" => { && arg != "help"
{
{ args.insert(1, "check".into());
args.insert(1, "check".into()); }
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.") }
}
},
_ => None
};
let args = Args::parse_from(args); let args = Args::parse_from(args);
match run(args, deprecated_alias_warning) { match run(args) {
Ok(code) => code.into(), Ok(code) => code.into(),
Err(err) => { Err(err) => {
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
@@ -81,3 +65,12 @@ pub fn main() -> ExitCode {
} }
} }
} }
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
match cmd {
"--explain" => "rule",
"--clean" => "clean",
"--generate-shell-completion" => "generate-shell-completion",
cmd => cmd,
}
}

View File

@@ -118,8 +118,6 @@ impl Printer {
} else if remaining > 0 { } else if remaining > 0 {
let s = if remaining == 1 { "" } else { "s" }; let s = if remaining == 1 { "" } else { "s" };
writeln!(writer, "Found {remaining} error{s}.")?; writeln!(writer, "Found {remaining} error{s}.")?;
} else if remaining == 0 {
writeln!(writer, "All checks passed!")?;
} }
if let Some(fixables) = fixables { if let Some(fixables) = fixables {

View File

@@ -1,4 +1,4 @@
use std::path::Path; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use log::debug; use log::debug;
@@ -11,17 +11,19 @@ use ruff_workspace::resolver::{
Relativity, Relativity,
}; };
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Resolve the relevant settings strategy and defaults for the current /// Resolve the relevant settings strategy and defaults for the current
/// invocation. /// invocation.
pub fn resolve( pub fn resolve(
config_arguments: &ConfigArguments, isolated: bool,
config: Option<&Path>,
overrides: &CliOverrides,
stdin_filename: Option<&Path>, stdin_filename: Option<&Path>,
) -> Result<PyprojectConfig> { ) -> Result<PyprojectConfig> {
// First priority: if we're running in isolated mode, use the default settings. // First priority: if we're running in isolated mode, use the default settings.
if config_arguments.isolated { if isolated {
let config = config_arguments.transform(Configuration::default()); let config = overrides.transform(Configuration::default());
let settings = config.into_settings(&path_dedot::CWD)?; let settings = config.into_settings(&path_dedot::CWD)?;
debug!("Isolated mode, not reading any pyproject.toml"); debug!("Isolated mode, not reading any pyproject.toml");
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
@@ -34,8 +36,12 @@ pub fn resolve(
// Second priority: the user specified a `pyproject.toml` file. Use that // Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the // `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.) // current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config_arguments.config_file() { if let Some(pyproject) = config
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?; .map(|config| config.display().to_string())
.map(|config| shellexpand::full(&config).map(|config| PathBuf::from(config.as_ref())))
.transpose()?
{
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
debug!( debug!(
"Using user-specified configuration file at: {}", "Using user-specified configuration file at: {}",
pyproject.display() pyproject.display()
@@ -43,7 +49,7 @@ pub fn resolve(
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed, PyprojectDiscoveryStrategy::Fixed,
settings, settings,
Some(pyproject.to_path_buf()), Some(pyproject),
)); ));
} }
@@ -61,7 +67,7 @@ pub fn resolve(
"Using configuration file (via parent) at: {}", "Using configuration file (via parent) at: {}",
pyproject.display() pyproject.display()
); );
let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?; let settings = resolve_root_settings(&pyproject, Relativity::Parent, overrides)?;
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,
settings, settings,
@@ -78,7 +84,7 @@ pub fn resolve(
"Using configuration file (via cwd) at: {}", "Using configuration file (via cwd) at: {}",
pyproject.display() pyproject.display()
); );
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?; let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,
settings, settings,
@@ -91,7 +97,7 @@ pub fn resolve(
// "closest" `pyproject.toml` file for every Python file later on, so these act // "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.) // as the "default" settings.)
debug!("Using Ruff default settings"); debug!("Using Ruff default settings");
let config = config_arguments.transform(Configuration::default()); let config = overrides.transform(Configuration::default());
let settings = config.into_settings(&path_dedot::CWD)?; let settings = config.into_settings(&path_dedot::CWD)?;
Ok(PyprojectConfig::new( Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,

View File

@@ -12,10 +12,9 @@ const STDIN: &str = "l = 1";
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command { fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME)); let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false))); let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
cmd.arg("check") cmd.arg("--output-format");
.arg("--output-format") cmd.arg(output_format);
.arg(output_format) cmd.arg("--no-cache");
.arg("--no-cache");
match show_source { match show_source {
Some(true) => { Some(true) => {
cmd.arg("--show-source"); cmd.arg("--show-source");

View File

@@ -7,15 +7,10 @@ use std::str;
use anyhow::Result; use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use regex::escape;
use tempfile::TempDir; use tempfile::TempDir;
const BIN_NAME: &str = "ruff"; const BIN_NAME: &str = "ruff";
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
}
#[test] #[test]
fn default_options() { fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
@@ -23,7 +18,7 @@ fn default_options() {
.arg("-") .arg("-")
.pass_stdin(r#" .pass_stdin(r#"
def foo(arg1, arg2,): def foo(arg1, arg2,):
print('Shouldn\'t change quotes') print('Should\'t change quotes')
if condition: if condition:
@@ -38,7 +33,7 @@ if condition:
arg1, arg1,
arg2, arg2,
): ):
print("Shouldn't change quotes") print("Should't change quotes")
if condition: if condition:
@@ -95,182 +90,6 @@ fn format_warn_stdin_filename_with_files() {
"###); "###);
} }
#[test]
fn nonexistent_config_file() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config", "foo.toml", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo.toml' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not point to a configuration file
For more information, try '--help'.
"###);
}
#[test]
fn config_override_rejected_if_invalid_toml() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config", "foo = bar", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 7
|
1 | foo = bar
| ^
invalid string
expected `"`, `'`
For more information, try '--help'.
"###);
}
#[test]
fn too_many_config_files() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
Ok(())
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--isolated")
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
Ok(())
}
#[test]
fn config_override_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(&ruff_toml, "line-length = 100")?;
let fixture = r#"
def foo():
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
"#;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_toml)
// This overrides the long line length set in the config file
.args(["--config", "line-length=80"])
.arg("-")
.pass_stdin(fixture), @r###"
success: true
exit_code: 0
----- stdout -----
def foo():
print(
"looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string"
)
----- stderr -----
"###);
Ok(())
}
#[test]
fn config_doubly_overridden_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(&ruff_toml, "line-length = 70")?;
let fixture = r#"
def foo():
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
"#;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_toml)
// This overrides the long line length set in the config file...
.args(["--config", "line-length=80"])
// ...but this overrides them both:
.args(["--line-length", "100"])
.arg("-")
.pass_stdin(fixture), @r###"
success: true
exit_code: 0
----- stdout -----
def foo():
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
----- stderr -----
"###);
Ok(())
}
#[test] #[test]
fn format_options() -> Result<()> { fn format_options() -> Result<()> {
let tempdir = TempDir::new()?; let tempdir = TempDir::new()?;
@@ -358,52 +177,58 @@ def f(x):
''' '''
pass pass
"#), @r###" "#), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
def f(x): def f(x):
""" """
Something about `f`. And an example: Something about `f`. And an example:
.. code-block:: python .. code-block:: python
foo, bar, quux = ( (
this_is_a_long_line( foo,
lion, bar,
hippo, quux,
lemur, ) = this_is_a_long_line(
bear, lion,
) hippo,
) lemur,
bear,
Another example:
```py
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
) )
```
And another: Another example:
>>> foo, bar, quux = ( ```py
... this_is_a_long_line( (
... lion, foo,
... hippo, bar,
... lemur, quux,
... bear, ) = this_is_a_long_line(
... ) lion,
... ) hippo,
""" lemur,
pass bear,
)
```
----- stderr ----- And another:
"###);
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
Ok(()) Ok(())
} }
@@ -683,9 +508,11 @@ if __name__ == '__main__':
say_hy("dear Ruff contributor") say_hy("dear Ruff contributor")
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
- 'ignore' -> 'lint.ignore' - 'ignore' -> 'lint.ignore'
"###); "###);
Ok(()) Ok(())
} }
@@ -724,9 +551,11 @@ if __name__ == '__main__':
say_hy("dear Ruff contributor") say_hy("dear Ruff contributor")
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
- 'ignore' -> 'lint.ignore' - 'ignore' -> 'lint.ignore'
"###); "###);
Ok(()) Ok(())
} }
@@ -1719,322 +1548,3 @@ include = ["*.ipy"]
"###); "###);
Ok(()) 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

View File

@@ -2,7 +2,6 @@
#![cfg(not(target_family = "wasm"))] #![cfg(not(target_family = "wasm"))]
use regex::escape;
use std::fs; use std::fs;
use std::process::Command; use std::process::Command;
use std::str; use std::str;
@@ -12,11 +11,7 @@ use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir; use tempfile::TempDir;
const BIN_NAME: &str = "ruff"; const BIN_NAME: &str = "ruff";
const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"]; 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] #[test]
fn top_level_options() -> Result<()> { fn top_level_options() -> Result<()> {
@@ -32,32 +27,29 @@ inline-quotes = "single"
"#, "#,
)?; )?;
insta::with_settings!({ assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] .args(STDIN_BASE_OPTIONS)
}, { .arg("--config")
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .arg(&ruff_toml)
.args(STDIN_BASE_OPTIONS) .args(["--stdin-filename", "test.py"])
.arg("--config") .arg("-")
.arg(&ruff_toml) .pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
.args(["--stdin-filename", "test.py"]) success: false
.arg("-") exit_code: 1
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###" ----- stdout -----
success: false test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
exit_code: 1 test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
----- stdout ----- test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred Found 3 errors.
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading [*] 2 fixable with the `--fix` option.
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
- 'flake8-quotes' -> 'lint.flake8-quotes' - 'flake8-quotes' -> 'lint.flake8-quotes'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -76,9 +68,6 @@ inline-quotes = "single"
"#, "#,
)?; )?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--config") .arg("--config")
@@ -96,8 +85,6 @@ inline-quotes = "single"
----- stderr ----- ----- stderr -----
"###); "###);
});
Ok(()) Ok(())
} }
@@ -116,9 +103,6 @@ inline-quotes = "single"
"#, "#,
)?; )?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--config") .arg("--config")
@@ -135,11 +119,11 @@ inline-quotes = "single"
[*] 2 fixable with the `--fix` option. [*] 2 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -162,9 +146,6 @@ inline-quotes = "single"
"#, "#,
)?; )?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--config") .arg("--config")
@@ -181,11 +162,11 @@ inline-quotes = "single"
[*] 2 fixable with the `--fix` option. [*] 2 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'flake8-quotes' -> 'lint.flake8-quotes' - 'flake8-quotes' -> 'lint.flake8-quotes'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -241,11 +222,9 @@ OTHER = "OTHER"
fs::write(out_dir.join("a.py"), r#"a = "a""#)?; 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)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path()) .current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude // Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
@@ -262,11 +241,11 @@ OTHER = "OTHER"
[*] 3 fixable with the `--fix` option. [*] 3 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -287,11 +266,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)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path()) .current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"]) .args(["--stdin-filename", "generated.py"])
@@ -311,11 +288,11 @@ if __name__ == "__main__":
[*] 2 fixable with the `--fix` option. [*] 2 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -334,9 +311,6 @@ 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)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--config") .arg("--config")
@@ -356,12 +330,12 @@ _ = "---------------------------------------------------------------------------
Found 1 error. Found 1 error.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'select' -> 'lint.select' - 'select' -> 'lint.select'
- 'pycodestyle' -> 'lint.pycodestyle' - 'pycodestyle' -> 'lint.pycodestyle'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -379,11 +353,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)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path()) .current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"]) .args(["--stdin-filename", "generated.py"])
@@ -405,11 +377,11 @@ if __name__ == "__main__":
[*] 1 fixable with the `--fix` option. [*] 1 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -427,11 +399,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)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path()) .current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"]) .args(["--stdin-filename", "generated.py"])
@@ -453,11 +423,11 @@ if __name__ == "__main__":
[*] 1 fixable with the `--fix` option. [*] 1 fixable with the `--fix` option.
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'extend-select' -> 'lint.extend-select' - 'extend-select' -> 'lint.extend-select'
"###);
});
"###);
Ok(()) Ok(())
} }
@@ -486,380 +456,21 @@ ignore = ["D203", "D212"]
"#, "#,
)?; )?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir) .current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
, @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
All checks passed!
----- stderr ----- ----- stderr -----
warning: No Python files found under the given path(s) warning: No Python files found under the given path(s)
"###); "###);
});
Ok(()) Ok(())
} }
#[test]
fn nonexistent_config_file() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "foo.toml", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo.toml' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not point to a configuration file
For more information, try '--help'.
"###);
}
#[test]
fn config_override_rejected_if_invalid_toml() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "foo = bar", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 7
|
1 | foo = bar
| ^
invalid string
expected `"`, `'`
For more information, try '--help'.
"###);
}
#[test]
fn too_many_config_files() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
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_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
Ok(())
}
#[test]
fn extend_passed_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "extend = 'foo.toml'", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend = 'foo.toml'' for '--config <CONFIG_OPTION>'
tip: Cannot include `extend` in a --config flag value
For more information, try '--help'.
"###);
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
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_dot_toml)
.arg("--isolated")
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
Ok(())
}
#[test]
fn config_override_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
line-length = 100
[lint]
select = ["I"]
[lint.isort]
combine-as-imports = true
"#,
)?;
let fixture = r#"
from foo import (
aaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbb as bbbbbbbbbbbbbbbb,
cccccccccccccccc,
ddddddddddd as ddddddddddddd,
eeeeeeeeeeeeeee,
ffffffffffff as ffffffffffffff,
ggggggggggggg,
hhhhhhh as hhhhhhhhhhh,
iiiiiiiiiiiiii,
jjjjjjjjjjjjj as jjjjjj,
)
x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
"#;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.args(["--config", "line-length=90"])
.args(["--config", "lint.extend-select=['E501', 'F841']"])
.args(["--config", "lint.isort.combine-as-imports = false"])
.arg("-")
.pass_stdin(fixture), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:1: I001 [*] Import block is un-sorted or un-formatted
-:15:91: E501 Line too long (97 > 90)
Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn valid_toml_but_nonexistent_option_provided_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args([".", "--config", "extend-select=['F481']"]), // No such code as F481!
@r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend-select=['F481']' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
Could not parse the supplied argument as a `ruff.toml` configuration option:
Unknown rule selector: `F481`
For more information, try '--help'.
"###);
}
#[test]
fn each_toml_option_requires_a_new_flag_1() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
// commas can't be used to delimit different config overrides;
// you need a new --config flag for each override
.args([".", "--config", "extend-select=['F841'], line-length=90"]),
@r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend-select=['F841'], line-length=90' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 23
|
1 | extend-select=['F841'], line-length=90
| ^
expected newline, `#`
For more information, try '--help'.
"###);
}
#[test]
fn each_toml_option_requires_a_new_flag_2() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
// spaces *also* can't be used to delimit different config overrides;
// you need a new --config flag for each override
.args([".", "--config", "extend-select=['F841'] line-length=90"]),
@r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend-select=['F841'] line-length=90' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 24
|
1 | extend-select=['F841'] line-length=90
| ^
expected newline, `#`
For more information, try '--help'.
"###);
}
#[test]
fn config_doubly_overridden_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
line-length = 100
[lint]
select=["E501"]
"#,
)?;
let fixture = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'";
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
// The --line-length flag takes priority over both the config file
// and the `--config="line-length=110"` flag,
// despite them both being specified after this flag on the command line:
.args(["--line-length", "90"])
.arg("--config")
.arg(&ruff_toml)
.args(["--config", "line-length=110"])
.arg("-")
.pass_stdin(fixture), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:91: E501 Line too long (97 > 90)
Found 1 error.
----- stderr -----
"###);
Ok(())
}
#[test]
fn complex_config_setting_overridden_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(&ruff_toml, "lint.select = ['N801']")?;
let fixture = "class violates_n801: pass";
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"])
.args(["--stdin-filename", "generated.py"])
.arg("-")
.pass_stdin(fixture), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}
#[test]
fn deprecated_config_option_overridden_via_cli() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "select=['N801']", "-"])
.pass_stdin("class lowercase: ..."),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:7: N801 Class name `lowercase` should use CapWords convention
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 your `--config` CLI arguments:
- 'select' -> 'lint.select'
"###);
}
#[test] #[test]
fn extension() -> Result<()> { fn extension() -> Result<()> {
let tempdir = TempDir::new()?; let tempdir = TempDir::new()?;
@@ -913,11 +524,9 @@ include = ["*.ipy"]
"#, "#,
)?; )?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path()) .current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--extension", "ipy:ipynb"]) .args(["--extension", "ipy:ipynb"])
@@ -931,240 +540,5 @@ include = ["*.ipy"]
----- stderr ----- ----- stderr -----
"###); "###);
});
Ok(())
}
#[test]
fn file_noqa_external() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
external = ["AAA"]
"#,
)?;
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)
.arg("-")
.pass_stdin(r#"
# flake8: noqa: AAA101, BBB102
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:3:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102
"###);
});
Ok(())
}
#[test]
fn required_version_exact_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = "0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_exact_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = "{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
#[test]
fn required_version_bound_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = ">{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_bound_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = ">=0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// Expand environment variables in `--config` paths provided via the CLI.
#[test]
fn config_expand() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
ruff_toml,
r#"
[lint]
select = ["F"]
ignore = ["F841"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg("${NAME}.toml")
.env("NAME", "ruff")
.arg("-")
.current_dir(tempdir.path())
.pass_stdin(r#"
import os
def func():
x = 1
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(()) Ok(())
} }

View File

@@ -39,8 +39,10 @@ fn check_project_include_defaults() {
[BASEPATH]/include-test/subdirectory/c.py [BASEPATH]/include-test/subdirectory/c.py
----- stderr ----- ----- 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`: warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your configuration:
- 'select' -> 'lint.select' - 'select' -> 'lint.select'
"###); "###);
}); });
} }

View File

@@ -4,29 +4,25 @@ use std::process::Command;
const BIN_NAME: &str = "ruff"; const BIN_NAME: &str = "ruff";
#[cfg(not(target_os = "windows"))]
const TEST_FILTERS: &[(&str, &str)] = &[
("\"[^\\*\"]*/pyproject.toml", "\"[BASEPATH]/pyproject.toml"),
("\".*/crates", "\"[BASEPATH]/crates"),
("\".*/\\.ruff_cache", "\"[BASEPATH]/.ruff_cache"),
("\".*/ruff\"", "\"[BASEPATH]\""),
];
#[cfg(target_os = "windows")]
const TEST_FILTERS: &[(&str, &str)] = &[
(r#""[^\*"]*\\pyproject.toml"#, "\"[BASEPATH]/pyproject.toml"),
(r#"".*\\crates"#, "\"[BASEPATH]/crates"),
(r#"".*\\\.ruff_cache"#, "\"[BASEPATH]/.ruff_cache"),
(r#"".*\\ruff""#, "\"[BASEPATH]\""),
(r#"\\+(\w\w|\s|")"#, "/$1"),
];
#[test] #[test]
fn display_default_settings() { fn display_default_settings() {
// Navigate from the crate directory to the workspace root. insta::with_settings!({ filters => TEST_FILTERS.to_vec() }, {
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let base_path = base_path.to_string_lossy();
// Escape the backslashes for the regex.
let base_path = regex::escape(&base_path);
#[cfg(not(target_os = "windows"))]
let test_filters = &[(base_path.as_ref(), "[BASEPATH]")];
#[cfg(target_os = "windows")]
let test_filters = &[
(base_path.as_ref(), "[BASEPATH]"),
(r#"\\+(\w\w|\s|\.|")"#, "/$1"),
];
insta::with_settings!({ filters => test_filters.to_vec() }, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures"))); .args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
}); });

View File

@@ -3,7 +3,7 @@ source: crates/ruff/tests/integration_test.rs
info: info:
program: ruff program: ruff
args: args:
- rule - "--explain"
- F401 - F401
--- ---
success: true success: true
@@ -25,20 +25,6 @@ import cycles. They also increase the cognitive load of reading the code.
If an import statement is used to check for the availability or existence If an import statement is used to check for the availability or existence
of a module, consider using `importlib.util.find_spec` instead. of a module, consider using `importlib.util.find_spec` instead.
If an import statement is used to re-export a symbol as part of a module's
public interface, consider using a "redundant" import alias, which
instructs Ruff (and other tools) to respect the re-export, and avoid
marking it as unused, as in:
```python
from module import member as member
```
## Fix safety
When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
These fixes are considered unsafe because they can change the public interface.
## Example ## Example
```python ```python
import numpy as np # unused import import numpy as np # unused import
@@ -65,11 +51,11 @@ else:
``` ```
## Options ## Options
- `lint.ignore-init-module-imports` - `lint.pyflakes.extend-generics`
## References ## References
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) - [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec) - [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
----- stderr ----- ----- stderr -----

View File

@@ -44,6 +44,7 @@ file_resolver.exclude = [
"__pypackages__", "__pypackages__",
"_build", "_build",
"buck-out", "buck-out",
"build",
"dist", "dist",
"node_modules", "node_modules",
"site-packages", "site-packages",
@@ -201,12 +202,10 @@ linter.allowed_confusables = []
linter.builtins = [] linter.builtins = []
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
linter.external = [] linter.external = []
linter.ignore_init_module_imports = true linter.ignore_init_module_imports = false
linter.logger_objects = [] linter.logger_objects = []
linter.namespace_packages = [] linter.namespace_packages = []
linter.src = [ linter.src = ["[BASEPATH]"]
"[BASEPATH]",
]
linter.tab_size = 4 linter.tab_size = 4
linter.line_length = 88 linter.line_length = 88
linter.task_tags = [ linter.task_tags = [
@@ -231,7 +230,7 @@ linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = [] linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = [] linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})* linter.flake8_copyright.notice_rgx = (?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.author = none linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0 linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0 linter.flake8_errmsg.max_string_length = 0
@@ -241,22 +240,7 @@ linter.flake8_gettext.functions_names = [
ngettext, ngettext,
] ]
linter.flake8_implicit_str_concat.allow_multiline = true linter.flake8_implicit_str_concat.allow_multiline = true
linter.flake8_import_conventions.aliases = { linter.flake8_import_conventions.aliases = {"matplotlib": "mpl", "matplotlib.pyplot": "plt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "networkx": "nx", "plotly.express": "px", "polars": "pl", "numpy": "np", "panel": "pn", "pyarrow": "pa", "altair": "alt", "tkinter": "tk", "holoviews": "hv"}
altair = alt,
holoviews = hv,
matplotlib = mpl,
matplotlib.pyplot = plt,
networkx = nx,
numpy = np,
pandas = pd,
panel = pn,
plotly.express = px,
polars = pl,
pyarrow = pa,
seaborn = sns,
tensorflow = tf,
tkinter = tk,
}
linter.flake8_import_conventions.banned_aliases = {} linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = [] linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = true linter.flake8_pytest_style.fixture_parentheses = true
@@ -326,7 +310,6 @@ linter.isort.section_order = [
known { type = first_party }, known { type = first_party },
known { type = local_folder }, known { type = local_folder },
] ]
linter.isort.default_section = known { type = third_party }
linter.isort.no_sections = false linter.isort.no_sections = false
linter.isort.from_first = false linter.isort.from_first = false
linter.isort.length_sort = false linter.isort.length_sort = false
@@ -381,3 +364,4 @@ formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic formatter.docstring_code_line_width = dynamic
----- stderr ----- ----- stderr -----

View File

@@ -1,105 +0,0 @@
//! Tests for the --version command
use std::fs;
use std::process::Command;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const VERSION_FILTER: [(&str, &str); 1] = [(
r"\d+\.\d+\.\d+(\+\d+)?( \(\w{9} \d\d\d\d-\d\d-\d\d\))?",
"[VERSION]",
)];
#[test]
fn version_basics() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}
/// `--config` is a global option,
/// so it's allowed to pass --config to subcommands such as `version`
/// -- the flag is simply ignored
#[test]
fn config_option_allowed_but_ignored() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.arg("--config")
.arg(&ruff_dot_toml)
.args(["--config", "lint.isort.extra-standard-library = ['foo', 'bar']"]), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
Ok(())
}
#[test]
fn config_option_ignored_but_validated() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.args(["--config", "foo = bar"]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 7
|
1 | foo = bar
| ^
invalid string
expected `"`, `'`
For more information, try '--help'.
"###
);
});
}
/// `--isolated` is also a global option,
#[test]
fn isolated_option_allowed() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version").arg("--isolated"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}

View File

@@ -13,7 +13,6 @@ license = { workspace = true }
[lib] [lib]
bench = false bench = false
doctest = false
[[bench]] [[bench]]
name = "linter" name = "linter"

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, GlobalConfigArgs, LogLevelArgs}; use ruff::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
use ruff::resolve::resolve; use ruff::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError}; use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel; use ruff_linter::logging::LogLevel;
@@ -38,21 +38,26 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError; use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArguments)> { fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
let args_matches = FormatCommand::command() let args_matches = FormatCommand::command()
.no_binary_name(true) .no_binary_name(true)
.get_matches_from(dirs); .get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?; let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, config_arguments) = arguments.partition(GlobalConfigArgs::default())?; let (cli, overrides) = arguments.partition();
Ok((cli, config_arguments)) Ok((cli, overrides))
} }
/// Find the [`PyprojectConfig`] to use for formatting. /// Find the [`PyprojectConfig`] to use for formatting.
fn find_pyproject_config( fn find_pyproject_config(
cli: &FormatArguments, cli: &FormatArguments,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
) -> anyhow::Result<PyprojectConfig> { ) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?; let mut pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
overrides,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml // We don't want to format pyproject.toml
pyproject_config.settings.file_resolver.include = FilePatternSet::try_from_iter([ pyproject_config.settings.file_resolver.include = FilePatternSet::try_from_iter([
FilePattern::Builtin("*.py"), FilePattern::Builtin("*.py"),
@@ -67,9 +72,9 @@ fn find_pyproject_config(
fn ruff_check_paths<'a>( fn ruff_check_paths<'a>(
pyproject_config: &'a PyprojectConfig, pyproject_config: &'a PyprojectConfig,
cli: &FormatArguments, cli: &FormatArguments,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> { ) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
Ok((paths, resolver)) Ok((paths, resolver))
} }

View File

@@ -4,8 +4,8 @@
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use ruff::{args::GlobalConfigArgs, check}; use ruff::check;
use ruff_linter::logging::set_up_logging; use ruff_linter::logging::{set_up_logging, LogLevel};
use std::process::ExitCode; use std::process::ExitCode;
mod format_dev; mod format_dev;
@@ -28,8 +28,6 @@ const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");
struct Args { struct Args {
#[command(subcommand)] #[command(subcommand)]
command: Command, command: Command,
#[clap(flatten)]
global_options: GlobalConfigArgs,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -59,6 +57,8 @@ enum Command {
Repeat { Repeat {
#[clap(flatten)] #[clap(flatten)]
args: ruff::args::CheckCommand, args: ruff::args::CheckCommand,
#[clap(flatten)]
log_level_args: ruff::args::LogLevelArgs,
/// Run this many times /// Run this many times
#[clap(long)] #[clap(long)]
repeat: usize, repeat: usize,
@@ -75,12 +75,9 @@ enum Command {
} }
fn main() -> Result<ExitCode> { fn main() -> Result<ExitCode> {
let Args { let args = Args::parse();
command,
global_options,
} = Args::parse();
#[allow(clippy::print_stdout)] #[allow(clippy::print_stdout)]
match command { match args.command {
Command::GenerateAll(args) => generate_all::main(&args)?, Command::GenerateAll(args) => generate_all::main(&args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
@@ -92,12 +89,14 @@ fn main() -> Result<ExitCode> {
Command::PrintTokens(args) => print_tokens::main(&args)?, Command::PrintTokens(args) => print_tokens::main(&args)?,
Command::RoundTrip(args) => round_trip::main(&args)?, Command::RoundTrip(args) => round_trip::main(&args)?,
Command::Repeat { Command::Repeat {
args: subcommand_args, args,
repeat, repeat,
log_level_args,
} => { } => {
set_up_logging(global_options.log_level())?; let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
for _ in 0..repeat { for _ in 0..repeat {
check(subcommand_args.clone(), global_options.clone())?; check(args.clone(), log_level)?;
} }
} }
Command::FormatDev(args) => { Command::FormatDev(args) => {

View File

@@ -11,7 +11,6 @@ repository = { workspace = true }
license = { workspace = true } license = { workspace = true }
[lib] [lib]
doctest = false
[dependencies] [dependencies]
ruff_text_size = { path = "../ruff_text_size" } ruff_text_size = { path = "../ruff_text_size" }

View File

@@ -37,7 +37,7 @@ pub trait Buffer {
#[doc(hidden)] #[doc(hidden)]
fn elements(&self) -> &[FormatElement]; fn elements(&self) -> &[FormatElement];
/// Glue for usage of the [`write!`] macro with implementers of this trait. /// Glue for usage of the [`write!`] macro with implementors of this trait.
/// ///
/// This method should generally not be invoked manually, but rather through the [`write!`] macro itself. /// This method should generally not be invoked manually, but rather through the [`write!`] macro itself.
/// ///

View File

@@ -308,8 +308,11 @@ impl std::fmt::Debug for Token {
/// assert_eq!(printed.as_code(), r#""Hello 'Ruff'""#); /// assert_eq!(printed.as_code(), r#""Hello 'Ruff'""#);
/// assert_eq!(printed.sourcemap(), [ /// assert_eq!(printed.sourcemap(), [
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(0) }, /// 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(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(13) },
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(14) },
/// SourceMarker { source: TextSize::new(20), dest: TextSize::new(14) }, /// SourceMarker { source: TextSize::new(20), dest: TextSize::new(14) },
/// ]); /// ]);
/// ///
@@ -325,30 +328,24 @@ pub struct SourcePosition(TextSize);
impl<Context> Format<Context> for SourcePosition { impl<Context> Format<Context> for SourcePosition {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> { 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)); f.write_element(FormatElement::SourcePosition(self.0));
Ok(()) Ok(())
} }
} }
/// Creates a text from a dynamic string. /// Creates a text from a dynamic string with its optional start-position in the source document.
///
/// This is done by allocating a new string internally. /// This is done by allocating a new string internally.
pub fn text(text: &str) -> Text { pub fn text(text: &str, position: Option<TextSize>) -> Text {
debug_assert_no_newlines(text); debug_assert_no_newlines(text);
Text { text } Text { text, position }
} }
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
pub struct Text<'a> { pub struct Text<'a> {
text: &'a str, text: &'a str,
position: Option<TextSize>,
} }
impl<Context> Format<Context> for Text<'_> impl<Context> Format<Context> for Text<'_>
@@ -356,6 +353,10 @@ where
Context: FormatContext, Context: FormatContext,
{ {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> { 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 { f.write_element(FormatElement::Text {
text: self.text.to_string().into_boxed_str(), text: self.text.to_string().into_boxed_str(),
text_width: TextWidth::from_text(self.text, f.options().indent_width()), text_width: TextWidth::from_text(self.text, f.options().indent_width()),
@@ -2285,7 +2286,7 @@ impl<Context, T> std::fmt::Debug for FormatWith<Context, T> {
/// let mut join = f.join_with(&separator); /// let mut join = f.join_with(&separator);
/// ///
/// for item in &self.items { /// for item in &self.items {
/// join.entry(&format_with(|f| write!(f, [text(item)]))); /// join.entry(&format_with(|f| write!(f, [text(item, None)])));
/// } /// }
/// join.finish() /// join.finish()
/// })), /// })),
@@ -2370,7 +2371,7 @@ where
/// let mut count = 0; /// let mut count = 0;
/// ///
/// let value = format_once(|f| { /// let value = format_once(|f| {
/// write!(f, [text(&std::format!("Formatted {count}."))]) /// write!(f, [text(&std::format!("Formatted {count}."), None)])
/// }); /// });
/// ///
/// format!(SimpleFormatContext::default(), [value]).expect("Formatting once works fine"); /// format!(SimpleFormatContext::default(), [value]).expect("Formatting once works fine");

View File

@@ -346,7 +346,10 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
} }
FormatElement::SourcePosition(position) => { FormatElement::SourcePosition(position) => {
write!(f, [text(&std::format!("source_position({position:?})"))])?; write!(
f,
[text(&std::format!("source_position({position:?})"), None)]
)?;
} }
FormatElement::LineSuffixBoundary => { FormatElement::LineSuffixBoundary => {
@@ -357,7 +360,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(f, [token("best_fitting(")])?; write!(f, [token("best_fitting(")])?;
if *mode != BestFittingMode::FirstLine { if *mode != BestFittingMode::FirstLine {
write!(f, [text(&std::format!("mode: {mode:?}, "))])?; write!(f, [text(&std::format!("mode: {mode:?}, "), None)])?;
} }
write!(f, [token("[")])?; write!(f, [token("[")])?;
@@ -389,14 +392,17 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!( write!(
f, f,
[ [
text(&std::format!("<interned {index}>")), text(&std::format!("<interned {index}>"), None),
space(), space(),
&&**interned, &&**interned,
] ]
)?; )?;
} }
Some(reference) => { Some(reference) => {
write!(f, [text(&std::format!("<ref interned *{reference}>"))])?; write!(
f,
[text(&std::format!("<ref interned *{reference}>"), None)]
)?;
} }
} }
} }
@@ -415,7 +421,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f, f,
[ [
token("<END_TAG_WITHOUT_START<"), token("<END_TAG_WITHOUT_START<"),
text(&std::format!("{:?}", tag.kind())), text(&std::format!("{:?}", tag.kind()), None),
token(">>"), token(">>"),
] ]
)?; )?;
@@ -430,9 +436,9 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
token(")"), token(")"),
soft_line_break_or_space(), soft_line_break_or_space(),
token("ERROR<START_END_TAG_MISMATCH<start: "), token("ERROR<START_END_TAG_MISMATCH<start: "),
text(&std::format!("{start_kind:?}")), text(&std::format!("{start_kind:?}"), None),
token(", end: "), token(", end: "),
text(&std::format!("{:?}", tag.kind())), text(&std::format!("{:?}", tag.kind()), None),
token(">>") token(">>")
] ]
)?; )?;
@@ -464,7 +470,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f, f,
[ [
token("align("), token("align("),
text(&count.to_string()), text(&count.to_string(), None),
token(","), token(","),
space(), space(),
] ]
@@ -476,7 +482,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f, f,
[ [
token("line_suffix("), token("line_suffix("),
text(&std::format!("{reserved_width:?}")), text(&std::format!("{reserved_width:?}"), None),
token(","), token(","),
space(), space(),
] ]
@@ -493,7 +499,11 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = group.id() { if let Some(group_id) = group.id() {
write!( write!(
f, f,
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),] [
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
)?; )?;
} }
@@ -514,7 +524,11 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = id { if let Some(group_id) = id {
write!( write!(
f, f,
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),] [
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
)?; )?;
} }
} }
@@ -547,7 +561,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f, f,
[ [
token("indent_if_group_breaks("), token("indent_if_group_breaks("),
text(&std::format!("\"{id:?}\"")), text(&std::format!("\"{id:?}\""), None),
token(","), token(","),
space(), space(),
] ]
@@ -567,7 +581,11 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = condition.group_id { if let Some(group_id) = condition.group_id {
write!( write!(
f, f,
[text(&std::format!("\"{group_id:?}\"")), token(","), space()] [
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
)?; )?;
} }
} }
@@ -577,7 +595,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f, f,
[ [
token("label("), token("label("),
text(&std::format!("\"{label_id:?}\"")), text(&std::format!("\"{label_id:?}\""), None),
token(","), token(","),
space(), space(),
] ]
@@ -646,7 +664,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
ContentArrayEnd, ContentArrayEnd,
token(")"), token(")"),
soft_line_break_or_space(), soft_line_break_or_space(),
text(&std::format!("<START_WITHOUT_END<{top:?}>>")), text(&std::format!("<START_WITHOUT_END<{top:?}>>"), None),
] ]
)?; )?;
} }
@@ -789,7 +807,7 @@ impl Format<IrFormatContext<'_>> for Condition {
f, f,
[ [
token("if_group_fits_on_line("), token("if_group_fits_on_line("),
text(&std::format!("\"{id:?}\"")), text(&std::format!("\"{id:?}\""), None),
token(")") token(")")
] ]
), ),
@@ -798,7 +816,7 @@ impl Format<IrFormatContext<'_>> for Condition {
f, f,
[ [
token("if_group_breaks("), token("if_group_breaks("),
text(&std::format!("\"{id:?}\"")), text(&std::format!("\"{id:?}\""), None),
token(")") token(")")
] ]
), ),

View File

@@ -32,7 +32,7 @@ pub trait MemoizeFormat<Context> {
/// let value = self.value.get(); /// let value = self.value.get();
/// self.value.set(value + 1); /// self.value.set(value + 1);
/// ///
/// write!(f, [text(&std::format!("Formatted {value} times."))]) /// write!(f, [text(&std::format!("Formatted {value} times."), None)])
/// } /// }
/// } /// }
/// ///
@@ -110,7 +110,7 @@ where
/// write!(f, [ /// write!(f, [
/// token("Count:"), /// token("Count:"),
/// space(), /// space(),
/// text(&std::format!("{current}")), /// text(&std::format!("{current}"), None),
/// hard_line_break() /// hard_line_break()
/// ])?; /// ])?;
/// ///

View File

@@ -41,7 +41,7 @@ use std::marker::PhantomData;
use std::num::{NonZeroU16, NonZeroU8, TryFromIntError}; use std::num::{NonZeroU16, NonZeroU8, TryFromIntError};
use crate::format_element::document::Document; use crate::format_element::document::Document;
use crate::printer::{Printer, PrinterOptions}; use crate::printer::{Printer, PrinterOptions, SourceMapGeneration};
pub use arguments::{Argument, Arguments}; pub use arguments::{Argument, Arguments};
pub use buffer::{ pub use buffer::{
Buffer, BufferExtensions, BufferSnapshot, Inspect, RemoveSoftLinesBuffer, VecBuffer, 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 format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
pub use group_id::GroupId; pub use group_id::GroupId;
use ruff_macros::CacheKey; use ruff_macros::CacheKey;
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
#[cfg_attr( #[cfg_attr(
@@ -269,6 +269,7 @@ impl FormatOptions for SimpleFormatOptions {
line_width: self.line_width, line_width: self.line_width,
indent_style: self.indent_style, indent_style: self.indent_style,
indent_width: self.indent_width, indent_width: self.indent_width,
source_map_generation: SourceMapGeneration::Enabled,
..PrinterOptions::default() ..PrinterOptions::default()
} }
} }
@@ -431,133 +432,6 @@ impl Printed {
pub fn take_verbatim_ranges(&mut self) -> Vec<TextRange> { pub fn take_verbatim_ranges(&mut self) -> Vec<TextRange> {
std::mem::take(&mut self.verbatim_ranges) 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
}
pub fn into_code(self) -> String {
self.code
}
/// The range the formatted code corresponds to in the source document.
pub fn source_range(&self) -> TextRange {
self.source_range
}
#[must_use]
pub fn with_code(self, code: String) -> Self {
Self { code, ..self }
}
} }
/// Public return type of the formatter /// Public return type of the formatter
@@ -579,7 +453,7 @@ pub type FormatResult<F> = Result<F, FormatError>;
/// impl Format<SimpleFormatContext> for Paragraph { /// impl Format<SimpleFormatContext> for Paragraph {
/// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> { /// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
/// write!(f, [ /// write!(f, [
/// text(&self.0), /// text(&self.0, None),
/// hard_line_break(), /// hard_line_break(),
/// ]) /// ])
/// } /// }

View File

@@ -21,7 +21,8 @@ impl<'a> LineSuffixes<'a> {
/// Takes all the pending line suffixes. /// Takes all the pending line suffixes.
pub(super) fn take_pending<'l>( pub(super) fn take_pending<'l>(
&'l mut self, &'l mut self,
) -> impl DoubleEndedIterator<Item = LineSuffixEntry<'a>> + 'l + ExactSizeIterator { ) -> impl Iterator<Item = LineSuffixEntry<'a>> + DoubleEndedIterator + 'l + ExactSizeIterator
{
self.suffixes.drain(..) self.suffixes.drain(..)
} }

View File

@@ -4,7 +4,7 @@ use drop_bomb::DebugDropBomb;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
pub use printer_options::*; pub use printer_options::*;
use ruff_text_size::{TextLen, TextSize}; use ruff_text_size::{Ranged, TextLen, TextSize};
use crate::format_element::document::Document; use crate::format_element::document::Document;
use crate::format_element::tag::{Condition, GroupMode}; use crate::format_element::tag::{Condition, GroupMode};
@@ -60,10 +60,7 @@ impl<'a> Printer<'a> {
document: &'a Document, document: &'a Document,
indent: u16, indent: u16,
) -> PrintResult<Printed> { ) -> PrintResult<Printed> {
let indentation = Indention::Level(indent); let mut stack = PrintCallStack::new(PrintElementArgs::new(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()); let mut queue: PrintQueue<'a> = PrintQueue::new(document.as_ref());
loop { loop {
@@ -76,9 +73,6 @@ impl<'a> Printer<'a> {
} }
} }
// Push any pending marker
self.push_marker();
Ok(Printed::new( Ok(Printed::new(
self.state.buffer, self.state.buffer,
None, None,
@@ -100,38 +94,42 @@ impl<'a> Printer<'a> {
let args = stack.top(); let args = stack.top();
match element { match element {
FormatElement::Space => self.print_text(Text::Token(" ")), FormatElement::Space => self.print_text(Text::Token(" "), None),
FormatElement::Token { text } => self.print_text(Text::Token(text)), FormatElement::Token { text } => self.print_text(Text::Token(text), None),
FormatElement::Text { text, text_width } => self.print_text(Text::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,
text_width: *text_width, text_width: *text_width,
}); },
None,
),
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()),
);
} }
FormatElement::Line(line_mode) => { FormatElement::Line(line_mode) => {
if args.mode().is_flat() if args.mode().is_flat()
&& matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace) && matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace)
{ {
if line_mode == &LineMode::SoftOrSpace { if line_mode == &LineMode::SoftOrSpace {
self.print_text(Text::Token(" ")); self.print_text(Text::Token(" "), None);
} }
} else if self.state.line_suffixes.has_pending() { } else if self.state.line_suffixes.has_pending() {
self.flush_line_suffixes(queue, stack, Some(element)); self.flush_line_suffixes(queue, stack, Some(element));
} else { } else {
// Only print a newline if the current line isn't already empty // Only print a newline if the current line isn't already empty
if self.state.line_width > 0 { if self.state.line_width > 0 {
self.push_marker();
self.print_char('\n'); self.print_char('\n');
} }
// Print a second line break if this is an empty line // Print a second line break if this is an empty line
if line_mode == &LineMode::Empty { if line_mode == &LineMode::Empty {
self.push_marker();
self.print_char('\n'); self.print_char('\n');
} }
@@ -144,11 +142,8 @@ impl<'a> Printer<'a> {
} }
FormatElement::SourcePosition(position) => { FormatElement::SourcePosition(position) => {
// The printer defers printing indents until the next text self.state.source_position = *position;
// is printed. Pushing the marker now would mean that the self.push_marker();
// 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 => { FormatElement::LineSuffixBoundary => {
@@ -440,7 +435,7 @@ impl<'a> Printer<'a> {
Ok(print_mode) Ok(print_mode)
} }
fn print_text(&mut self, text: Text) { fn print_text(&mut self, text: Text, source_range: Option<TextRange>) {
if !self.state.pending_indent.is_empty() { if !self.state.pending_indent.is_empty() {
let (indent_char, repeat_count) = match self.options.indent_style() { let (indent_char, repeat_count) = match self.options.indent_style() {
IndentStyle::Tab => ('\t', 1), IndentStyle::Tab => ('\t', 1),
@@ -463,6 +458,19 @@ 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(); self.push_marker();
match text { match text {
@@ -485,24 +493,29 @@ impl<'a> Printer<'a> {
} }
} }
} }
if let Some(range) = source_range {
self.state.source_position = range.end();
}
self.push_marker();
} }
fn push_marker(&mut self) { fn push_marker(&mut self) {
let Some(source_position) = self.state.pending_source_position.take() else { if self.options.source_map_generation.is_disabled() {
return; return;
}; }
let marker = SourceMarker { let marker = SourceMarker {
source: source_position, source: self.state.source_position,
dest: self.state.buffer.text_len(), dest: self.state.buffer.text_len(),
}; };
if self if let Some(last) = self.state.source_markers.last() {
.state if last != &marker {
.source_markers self.state.source_markers.push(marker);
.last() }
.map_or(true, |last| last != &marker) } else {
{
self.state.source_markers.push(marker); self.state.source_markers.push(marker);
} }
} }
@@ -874,7 +887,7 @@ enum FillPairLayout {
struct PrinterState<'a> { struct PrinterState<'a> {
buffer: String, buffer: String,
source_markers: Vec<SourceMarker>, source_markers: Vec<SourceMarker>,
pending_source_position: Option<TextSize>, source_position: TextSize,
pending_indent: Indention, pending_indent: Indention,
measured_group_fits: bool, measured_group_fits: bool,
line_width: u32, line_width: u32,
@@ -1729,7 +1742,7 @@ a",
let result = format_with_options( let result = format_with_options(
&format_args![ &format_args![
token("function main() {"), token("function main() {"),
block_indent(&text("let x = `This is a multiline\nstring`;")), block_indent(&text("let x = `This is a multiline\nstring`;", None)),
token("}"), token("}"),
hard_line_break() hard_line_break()
], ],
@@ -1746,7 +1759,7 @@ a",
fn it_breaks_a_group_if_a_string_contains_a_newline() { fn it_breaks_a_group_if_a_string_contains_a_newline() {
let result = format(&FormatArrayElements { let result = format(&FormatArrayElements {
items: vec![ items: vec![
&text("`This is a string spanning\ntwo lines`"), &text("`This is a string spanning\ntwo lines`", None),
&token("\"b\""), &token("\"b\""),
], ],
}); });

View File

@@ -14,6 +14,10 @@ pub struct PrinterOptions {
/// The type of line ending to apply to the printed input /// The type of line ending to apply to the printed input
pub line_ending: LineEnding, 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 impl<'a, O> From<&'a O> for PrinterOptions

View File

@@ -78,28 +78,27 @@ impl<'a> PrintQueue<'a> {
impl<'a> Queue<'a> for PrintQueue<'a> { impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> { fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?; let elements = self.element_slices.last_mut()?;
elements.next().or_else( elements.next().or_else(|| {
#[cold] self.element_slices.pop();
|| { let elements = self.element_slices.last_mut()?;
self.element_slices.pop(); elements.next()
let elements = self.element_slices.last_mut()?; })
elements.next()
},
)
} }
fn top_with_interned(&self) -> Option<&'a FormatElement> { fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev(); let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?; let slice = slices.next()?;
slice.as_slice().first().or_else( match slice.as_slice().first() {
#[cold] Some(element) => Some(element),
|| { None => {
slices if let Some(next_elements) = slices.next() {
.next() next_elements.as_slice().first()
.and_then(|next_elements| next_elements.as_slice().first()) } else {
}, None
) }
}
}
} }
fn extend_back(&mut self, elements: &'a [FormatElement]) { fn extend_back(&mut self, elements: &'a [FormatElement]) {
@@ -147,30 +146,24 @@ impl<'a, 'print> FitsQueue<'a, 'print> {
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> { impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> { fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else( self.queue.pop().or_else(|| {
#[cold] if let Some(next_slice) = self.rest_elements.next_back() {
|| { self.queue.extend_back(next_slice.as_slice());
if let Some(next_slice) = self.rest_elements.next_back() { self.queue.pop()
self.queue.extend_back(next_slice.as_slice()); } else {
self.queue.pop() None
} else { }
None })
}
},
)
} }
fn top_with_interned(&self) -> Option<&'a FormatElement> { fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else( self.queue.top_with_interned().or_else(|| {
#[cold] if let Some(next_elements) = self.rest_elements.as_slice().last() {
|| { next_elements.as_slice().first()
if let Some(next_elements) = self.rest_elements.as_slice().last() { } else {
next_elements.as_slice().first() None
} else { }
None })
}
},
)
} }
fn extend_back(&mut self, elements: &'a [FormatElement]) { fn extend_back(&mut self, elements: &'a [FormatElement]) {

View File

@@ -11,7 +11,6 @@ repository = { workspace = true }
license = { workspace = true } license = { workspace = true }
[lib] [lib]
doctest = false
[dependencies] [dependencies]
ruff_macros = { path = "../ruff_macros" } ruff_macros = { path = "../ruff_macros" }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ruff_linter" name = "ruff_linter"
version = "0.3.2" version = "0.1.15"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@@ -60,6 +60,7 @@ regex = { workspace = true }
result-like = { workspace = true } result-like = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
similar = { workspace = true } similar = { workspace = true }
@@ -84,8 +85,6 @@ tempfile = { workspace = true }
[features] [features]
default = [] default = []
schemars = ["dep:schemars"] schemars = ["dep:schemars"]
# Enables rules for internal integration tests
test-rules = []
[lints] [lints]
workspace = true workspace = true

View File

@@ -28,11 +28,3 @@ dictionary = {
} }
#import os # noqa #import os # noqa
# case 1:
# try:
# try: # with comment
# try: print()
# except:
# except Foo:
# except Exception as e: print(e)

View File

@@ -18,7 +18,3 @@ func("0.0.0.0")
def my_func(): def my_func():
x = "0.0.0.0" x = "0.0.0.0"
print(x) print(x)
# Implicit string concatenation
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"

View File

@@ -18,13 +18,6 @@ with open("/dev/shm/unit/test", "w") as f:
with open("/foo/bar", "w") as f: with open("/foo/bar", "w") as f:
f.write("def") f.write("def")
# Implicit string concatenation
with open("/tmp/" "abc", "w") as f:
f.write("def")
with open("/tmp/abc" f"/tmp/abc", "w") as f:
f.write("def")
# Using `tempfile` module should be ok # Using `tempfile` module should be ok
import tempfile import tempfile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory

View File

@@ -1,22 +0,0 @@
from django.utils.safestring import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'
from django.utils.html import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'

View File

@@ -1,22 +0,0 @@
import os
import random
import a_lib
# OK
random.SystemRandom()
# Errors
random.Random()
random.random()
random.randrange()
random.randint()
random.choice()
random.choices()
random.uniform()
random.triangular()
random.randbytes()
# Unrelated
os.urandom()
a_lib.random()

View File

@@ -1,47 +1,52 @@
import crypt
import hashlib import hashlib
from hashlib import new as hashlib_new from hashlib import new as hashlib_new
from hashlib import sha1 as hashlib_sha1 from hashlib import sha1 as hashlib_sha1
# Errors # Invalid
hashlib.new('md5') hashlib.new('md5')
hashlib.new('md4', b'test') hashlib.new('md4', b'test')
hashlib.new(name='md5', data=b'test') hashlib.new(name='md5', data=b'test')
hashlib.new('MD4', data=b'test') hashlib.new('MD4', data=b'test')
hashlib.new('sha1') hashlib.new('sha1')
hashlib.new('sha1', data=b'test') hashlib.new('sha1', data=b'test')
hashlib.new('sha', data=b'test') hashlib.new('sha', data=b'test')
hashlib.new(name='SHA', data=b'test') hashlib.new(name='SHA', data=b'test')
hashlib.sha(data=b'test') hashlib.sha(data=b'test')
hashlib.md5() hashlib.md5()
hashlib_new('sha1') hashlib_new('sha1')
hashlib_sha1('sha1') hashlib_sha1('sha1')
# usedforsecurity arg only available in Python 3.9+ # usedforsecurity arg only available in Python 3.9+
hashlib.new('sha1', usedforsecurity=True) hashlib.new('sha1', usedforsecurity=True)
crypt.crypt("test", salt=crypt.METHOD_CRYPT) # Valid
crypt.crypt("test", salt=crypt.METHOD_MD5)
crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
crypt.crypt("test", crypt.METHOD_BLOWFISH)
crypt.mksalt(crypt.METHOD_CRYPT)
crypt.mksalt(crypt.METHOD_MD5)
crypt.mksalt(crypt.METHOD_BLOWFISH)
# OK
hashlib.new('sha256') hashlib.new('sha256')
hashlib.new('SHA512') hashlib.new('SHA512')
hashlib.sha256(data=b'test') hashlib.sha256(data=b'test')
# usedforsecurity arg only available in Python 3.9+ # usedforsecurity arg only available in Python 3.9+
hashlib_new(name='sha1', usedforsecurity=False) hashlib_new(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib_sha1(name='sha1', usedforsecurity=False) hashlib_sha1(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.md4(usedforsecurity=False) hashlib.md4(usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.new(name='sha256', usedforsecurity=False) hashlib.new(name='sha256', usedforsecurity=False)
crypt.crypt("test")
crypt.crypt("test", salt=crypt.METHOD_SHA256)
crypt.crypt("test", salt=crypt.METHOD_SHA512)
crypt.mksalt()
crypt.mksalt(crypt.METHOD_SHA256)
crypt.mksalt(crypt.METHOD_SHA512)

View File

@@ -1,5 +1,4 @@
import os import os
import subprocess
import commands import commands
import popen2 import popen2
@@ -17,8 +16,6 @@ popen2.Popen3("true")
popen2.Popen4("true") popen2.Popen4("true")
commands.getoutput("true") commands.getoutput("true")
commands.getstatusoutput("true") commands.getstatusoutput("true")
subprocess.getoutput("true")
subprocess.getstatusoutput("true")
# Check command argument looks unsafe. # Check command argument looks unsafe.

View File

@@ -1,34 +0,0 @@
from django.contrib.auth.models import User
# Errors
User.objects.filter(username='admin').extra(dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
User.objects.filter(username='admin').extra(select={'test': query})
where_var = ['1=1) OR 1=1 AND (1=1']
User.objects.filter(username='admin').extra(where=where_var)
where_str = '1=1) OR 1=1 AND (1=1'
User.objects.filter(username='admin').extra(where=[where_str])
tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
User.objects.all().extra(tables=tables_var).distinct()
tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
User.objects.all().extra(tables=[tables_str]).distinct()
# OK
User.objects.filter(username='admin').extra(
select={'test': 'secure'},
where=['secure'],
tables=['secure']
)
User.objects.filter(username='admin').extra({'test': 'secure'})
User.objects.filter(username='admin').extra(select={'test': 'secure'})
User.objects.filter(username='admin').extra(where=['secure'])

View File

@@ -119,16 +119,3 @@ def func(x: bool):
settings(True) settings(True)
from dataclasses import dataclass, InitVar
@dataclass
class Fit:
force: InitVar[bool] = False
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)

View File

@@ -13,11 +13,3 @@ 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 = 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)
])

View File

@@ -20,10 +20,3 @@ f"{dict(x='y') | dict(y='z')}"
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"
f"a { dict(x='y') | dict(y='z') } b" f"a { dict(x='y') | dict(y='z') } b"
dict(
# comment
)
tuple( # comment
)

View File

@@ -8,11 +8,3 @@ t4 = tuple([
t5 = tuple( t5 = tuple(
(1, 2) (1, 2)
) )
tuple( # comment
[1, 2]
)
tuple([ # comment
1, 2
])

View File

@@ -2,12 +2,3 @@ l1 = list([1, 2])
l2 = list((1, 2)) l2 = list((1, 2))
l3 = list([]) l3 = list([])
l4 = list(()) l4 = list(())
list( # comment
[1, 2]
)
list([ # comment
1, 2
])

View File

@@ -14,6 +14,9 @@ reversed(sorted(x, reverse=not x))
reversed(sorted(i for i in range(42))) reversed(sorted(i for i in range(42)))
reversed(sorted((i for i in range(42)), reverse=True)) reversed(sorted((i for i in range(42)), reverse=True))
# Regression test for: https://github.com/astral-sh/ruff/issues/10335
reversed(sorted([1, 2, 3], reverse=False or True)) def reversed(*args, **kwargs):
reversed(sorted([1, 2, 3], reverse=(False or True))) return None
reversed(sorted(x, reverse=True))

View File

@@ -7,19 +7,7 @@ from pdb import set_trace as st
from celery.contrib.rdb import set_trace from celery.contrib.rdb import set_trace
from celery.contrib import rdb from celery.contrib import rdb
import celery.contrib.rdb import celery.contrib.rdb
from debugpy import wait_for_client
import debugpy
from ptvsd import break_into_debugger
from ptvsd import enable_attach
from ptvsd import wait_for_attach
import ptvsd
breakpoint() breakpoint()
st() st()
set_trace() set_trace()
debugpy.breakpoint()
wait_for_client()
debugpy.listen(1234)
enable_attach()
break_into_debugger()
wait_for_attach()

View File

@@ -207,23 +207,3 @@ class Repro:
def stub(self) -> str: def stub(self) -> str:
"""Docstring""" """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()

View File

@@ -11,25 +11,13 @@ class _UnusedTypedDict2(typing.TypedDict):
class _UsedTypedDict(TypedDict): class _UsedTypedDict(TypedDict):
foo: bytes foo: bytes
class _CustomClass(_UsedTypedDict): class _CustomClass(_UsedTypedDict):
bar: list[int] bar: list[int]
_UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int}) _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ... def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
# In `.py` files, we don't flag unused definitions in class scopes (unlike in `.pyi`
# files).
class _CustomClass3:
class _UnusedTypeDict4(TypedDict):
pass
def method(self) -> None:
_CustomClass3._UnusedTypeDict4()

View File

@@ -35,13 +35,3 @@ _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ... def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
# In `.pyi` files, we flag unused definitions in class scopes as well as in the global
# scope (unlike in `.py` files).
class _CustomClass3:
class _UnusedTypeDict4(TypedDict):
pass
def method(self) -> None:
_CustomClass3._UnusedTypeDict4()

View File

@@ -64,5 +64,3 @@ def not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053 "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
) )
def not_a_deprecated_function() -> None: ... def not_a_deprecated_function() -> None: ...
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053

View File

@@ -40,7 +40,4 @@ f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004
# Make sure we do not unescape quotes # Make sure we do not unescape quotes
this_is_fine = "This is an \\'escaped\\' quote" this_is_fine = "This is an \\'escaped\\' quote"
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004 this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash"
# Invalid escapes in bytestrings are also triggered:
x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004

View File

@@ -93,15 +93,3 @@ def func():
# OK # OK
raise func() raise func()
# OK
future = executor.submit(float, "a")
if future.exception():
raise future.exception()
# RSE102
future = executor.submit(float, "a")
if future.exception():
raise future.Exception()

View File

@@ -192,49 +192,3 @@ elif x == 2:
y = "b" y = "b"
else: else:
y = "c" 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

View File

@@ -193,11 +193,3 @@ def func():
for y in range(5): for y in range(5):
g(x, idx) g(x, idx)
idx += 1 idx += 1
async def func():
# OK (for loop is async)
idx = 0
async for x in async_gen():
g(x, idx)
idx += 1

View File

@@ -1,27 +1,18 @@
import trio import trio
async def func(): async def foo():
with trio.fail_after(): with trio.fail_after():
... ...
async def foo():
async def func():
with trio.fail_at(): with trio.fail_at():
await ... await ...
async def foo():
async def func():
with trio.move_on_after(): with trio.move_on_after():
... ...
async def foo():
async def func():
with trio.move_at(): with trio.move_at():
await ... await ...
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -10,7 +10,7 @@ async def func():
trio.sleep(0) # TRIO115 trio.sleep(0) # TRIO115
foo = 0 foo = 0
trio.sleep(foo) # OK trio.sleep(foo) # TRIO115
trio.sleep(1) # OK trio.sleep(1) # OK
time.sleep(0) # OK time.sleep(0) # OK
@@ -20,26 +20,26 @@ async def func():
trio.sleep(bar) trio.sleep(bar)
x, y = 0, 2000 x, y = 0, 2000
trio.sleep(x) # OK trio.sleep(x) # TRIO115
trio.sleep(y) # OK trio.sleep(y) # OK
(a, b, [c, (d, e)]) = (1, 2, (0, [4, 0])) (a, b, [c, (d, e)]) = (1, 2, (0, [4, 0]))
trio.sleep(c) # OK trio.sleep(c) # TRIO115
trio.sleep(d) # OK trio.sleep(d) # OK
trio.sleep(e) # OK trio.sleep(e) # TRIO115
m_x, m_y = 0 m_x, m_y = 0
trio.sleep(m_y) # OK trio.sleep(m_y) # OK
trio.sleep(m_x) # OK trio.sleep(m_x) # OK
m_a = m_b = 0 m_a = m_b = 0
trio.sleep(m_a) # OK trio.sleep(m_a) # TRIO115
trio.sleep(m_b) # OK trio.sleep(m_b) # TRIO115
m_c = (m_d, m_e) = (0, 0) m_c = (m_d, m_e) = (0, 0)
trio.sleep(m_c) # OK trio.sleep(m_c) # OK
trio.sleep(m_d) # OK trio.sleep(m_d) # TRIO115
trio.sleep(m_e) # OK trio.sleep(m_e) # TRIO115
def func(): def func():
@@ -63,16 +63,4 @@ def func():
import trio import trio
if (walrus := 0) == 0: if (walrus := 0) == 0:
trio.sleep(walrus) # OK trio.sleep(walrus) # TRIO115
def func():
import trio
async def main() -> None:
sleep = 0
for _ in range(2):
await trio.sleep(sleep) # OK
sleep = 10
trio.run(main)

View File

@@ -0,0 +1,18 @@
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

View File

@@ -0,0 +1,16 @@
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

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,9 +0,0 @@
from __future__ import annotations
import django.settings
from library import foo
import os
import pytz
import sys
from . import local

View File

@@ -1,16 +0,0 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
class Thing(object):
name: str
def __init__(self, name: str):
self.name = name

View File

@@ -1,9 +0,0 @@
from __future__ import annotations
import os
import django.settings
from library import foo
import pytz
from . import local
import sys

View File

@@ -19,11 +19,8 @@ numpy.random.seed()
numpy.random.get_state() numpy.random.get_state()
numpy.random.set_state() numpy.random.set_state()
numpy.random.rand() numpy.random.rand()
numpy.random.ranf()
numpy.random.sample()
numpy.random.randn() numpy.random.randn()
numpy.random.randint() numpy.random.randint()
numpy.random.random()
numpy.random.random_integers() numpy.random.random_integers()
numpy.random.random_sample() numpy.random.random_sample()
numpy.random.choice() numpy.random.choice()
@@ -38,6 +35,7 @@ numpy.random.exponential()
numpy.random.f() numpy.random.f()
numpy.random.gamma() numpy.random.gamma()
numpy.random.geometric() numpy.random.geometric()
numpy.random.get_state()
numpy.random.gumbel() numpy.random.gumbel()
numpy.random.hypergeometric() numpy.random.hypergeometric()
numpy.random.laplace() numpy.random.laplace()

View File

@@ -50,29 +50,6 @@ class MetaClass(ABCMeta):
def static_method(not_cls) -> bool: def static_method(not_cls) -> bool:
return False return False
class ClsInArgsClass(ABCMeta):
def cls_as_argument(this, cls):
pass
def cls_as_pos_only_argument(this, cls, /):
pass
def cls_as_kw_only_argument(this, *, cls):
pass
def cls_as_varags(this, *cls):
pass
def cls_as_kwargs(this, **cls):
pass
class RenamingInMethodBodyClass(ABCMeta):
def bad_method(this):
this = this
this
def bad_method(this):
self = this
def func(x): def func(x):
return x return x

View File

@@ -61,7 +61,7 @@ class PosOnlyClass:
def good_method_pos_only(self, blah, /, something: str): def good_method_pos_only(self, blah, /, something: str):
pass pass
def bad_method_pos_only(this, blah, /, something: str): def bad_method_pos_only(this, blah, /, self, something: str):
pass pass
@@ -93,27 +93,3 @@ class ModelClass:
@foobar.thisisstatic @foobar.thisisstatic
def badstatic(foo): def badstatic(foo):
pass pass
class SelfInArgsClass:
def self_as_argument(this, self):
pass
def self_as_pos_only_argument(this, self, /):
pass
def self_as_kw_only_argument(this, *, self):
pass
def self_as_varags(this, *self):
pass
def self_as_kwargs(this, **self):
pass
class RenamingInMethodBodyClass:
def bad_method(this):
this = this
this
def bad_method(this):
self = this

View File

@@ -36,47 +36,35 @@ for i in list( # Comment
): # PERF101 ): # PERF101
pass pass
for i in list(foo_dict): # OK for i in list(foo_dict): # Ok
pass pass
for i in list(1): # OK for i in list(1): # Ok
pass pass
for i in list(foo_int): # OK for i in list(foo_int): # Ok
pass pass
import itertools import itertools
for i in itertools.product(foo_int): # OK for i in itertools.product(foo_int): # Ok
pass pass
for i in list(foo_list): # OK for i in list(foo_list): # Ok
foo_list.append(i + 1) foo_list.append(i + 1)
for i in list(foo_list): # PERF101 for i in list(foo_list): # PERF101
# Make sure we match the correct list # Make sure we match the correct list
other_list.append(i + 1) other_list.append(i + 1)
for i in list(foo_tuple): # OK for i in list(foo_tuple): # Ok
foo_tuple.append(i + 1) foo_tuple.append(i + 1)
for i in list(foo_set): # OK for i in list(foo_set): # Ok
foo_set.append(i + 1) foo_set.append(i + 1)
x, y, nested_tuple = (1, 2, (3, 4, 5)) x, y, nested_tuple = (1, 2, (3, 4, 5))
for i in list(nested_tuple): # PERF101 for i in list(nested_tuple): # PERF101
pass pass
for i in list(foo_list): # OK
if True:
foo_list.append(i + 1)
for i in list(foo_list): # OK
if True:
foo_list[i] = i + 1
for i in list(foo_list): # OK
if True:
del foo_list[i + 1]

View File

@@ -1,8 +0,0 @@
# These rules test for intentional "odd" formatting. Using a formatter fixes that
[E{1,2,3}*.py]
generated_code = true
ij_formatter_enabled = false
[W*.py]
generated_code = true
ij_formatter_enabled = false

View File

@@ -147,15 +147,3 @@ ham[upper : ]
#: E203:1:10 #: E203:1:10
ham[upper :] ham[upper :]
#: Okay
ham[lower +1 :, "columnname"]
#: E203:1:13
ham[lower + 1 :, "columnname"]
#: Okay
f"{ham[lower +1 :, "columnname"]}"
#: E203:1:13
f"{ham[lower + 1 :, "columnname"]}"

View File

@@ -1 +0,0 @@
a = (1 or)

View File

@@ -1,871 +0,0 @@
"""Fixtures for the errors E301, E302, E303, E304, E305 and E306.
Since these errors are about new lines, each test starts with either "No error" or "# E30X".
Each test's end is signaled by a "# end" line.
There should be no E30X error outside of a test's bound.
"""
# No error
class Class:
pass
# end
# No error
class Class:
"""Docstring"""
def __init__(self) -> None:
pass
# end
# No error
def func():
pass
# end
# No error
# comment
class Class:
pass
# end
# No error
# comment
def func():
pass
# end
# no error
def foo():
pass
def bar():
pass
class Foo(object):
pass
class Bar(object):
pass
# end
# No error
class Class(object):
def func1():
pass
def func2():
pass
# end
# No error
class Class(object):
def func1():
pass
# comment
def func2():
pass
# end
# No error
class Class:
def func1():
pass
# comment
def func2():
pass
# This is a
# ... multi-line comment
def func3():
pass
# This is a
# ... multi-line comment
@decorator
class Class:
def func1():
pass
# comment
def func2():
pass
@property
def func3():
pass
# end
# No error
try:
from nonexistent import Bar
except ImportError:
class Bar(object):
"""This is a Bar replacement"""
# end
# No error
def with_feature(f):
"""Some decorator"""
wrapper = f
if has_this_feature(f):
def wrapper(*args):
call_feature(args[0])
return f(*args)
return wrapper
# end
# No error
try:
next
except NameError:
def next(iterator, default):
for item in iterator:
return item
return default
# end
# No error
def fn():
pass
class Foo():
"""Class Foo"""
def fn():
pass
# end
# No error
# comment
def c():
pass
# comment
def d():
pass
# This is a
# ... multi-line comment
# And this one is
# ... a second paragraph
# ... which spans on 3 lines
# Function `e` is below
# NOTE: Hey this is a testcase
def e():
pass
def fn():
print()
# comment
print()
print()
# Comment 1
# Comment 2
# Comment 3
def fn2():
pass
# end
# no error
if __name__ == '__main__':
foo()
# end
# no error
defaults = {}
defaults.update({})
# end
# no error
def foo(x):
classification = x
definitely = not classification
# end
# no error
def bar(): pass
def baz(): pass
# end
# no error
def foo():
def bar(): pass
def baz(): pass
# end
# no error
from typing import overload
from typing import Union
# end
# no error
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
# end
# no error
def f(x: Union[int, str]) -> Union[int, str]:
return x
# end
# no error
from typing import Protocol
class C(Protocol):
@property
def f(self) -> int: ...
@property
def g(self) -> str: ...
# end
# no error
def f(
a,
):
pass
# end
# no error
if True:
class Class:
"""Docstring"""
def function(self):
...
# end
# no error
if True:
def function(self):
...
# end
# no error
@decorator
# comment
@decorator
def function():
pass
# end
# no error
class Class:
def method(self):
if True:
def function():
pass
# end
# no error
@decorator
async def function(data: None) -> None:
...
# end
# no error
class Class:
def method():
"""docstring"""
# comment
def function():
pass
# end
# no error
try:
if True:
# comment
class Class:
pass
except:
pass
# end
# no error
def f():
def f():
pass
# end
# no error
class MyClass:
# comment
def method(self) -> None:
pass
# end
# no error
def function1():
# Comment
def function2():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
if (
cond1
and cond2
):
pass
#end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
class Test:
async
def a(self): pass
# end
# no error
class Test:
def a():
pass
# wrongly indented comment
def b():
pass
# end
# no error
def test():
pass
# Wrongly indented comment
pass
# end
# E301
class Class(object):
def func1():
pass
def func2():
pass
# end
# E301
class Class:
def fn1():
pass
# comment
def fn2():
pass
# end
# E301
class Class:
"""Class for minimal repo."""
columns = []
@classmethod
def cls_method(cls) -> None:
pass
# end
# E301
class Class:
"""Class for minimal repo."""
def method(cls) -> None:
pass
@classmethod
def cls_method(cls) -> None:
pass
# end
# E302
"""Main module."""
def fn():
pass
# end
# E302
import sys
def get_sys_path():
return sys.path
# end
# E302
def a():
pass
def b():
pass
# end
# E302
def a():
pass
# comment
def b():
pass
# end
# E302
def a():
pass
async def b():
pass
# end
# E302
async def x():
pass
async def x(y: int = 1):
pass
# end
# E302
def bar():
pass
def baz(): pass
# end
# E302
def bar(): pass
def baz():
pass
# end
# E302
def f():
pass
# comment
@decorator
def g():
pass
# end
# E302
class Test:
pass
def method1():
return 1
def method2():
return 22
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
print()
print()
# end
# E303:5:1
print()
# comment
print()
# end
# E303:5:5 E303:8:5
def a():
print()
# comment
# another comment
print()
# end
# E303
#!python
"""This class docstring comes on line 5.
It gives error E303: too many blank lines (3)
"""
# end
# E303
class Class:
def a(self):
pass
def b(self):
pass
# end
# E303
if True:
a = 1
a = 2
# end
# E303
class Test:
# comment
# another comment
def test(self): pass
# end
# E303
class Test:
def a(self):
pass
# wrongly indented comment
def b(self):
pass
# end
# E303
def fn():
pass
pass
# end
# E304
@decorator
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
# second comment E304 not expected
def function():
pass
# end
# E305:7:1
def fn():
print()
# comment
# another comment
fn()
# end
# E305
class Class():
pass
# comment
# another comment
a = 1
# end
# E305:8:1
def fn():
print()
# comment
# another comment
try:
fn()
except Exception:
pass
# end
# E305:5:1
def a():
print()
# Two spaces before comments, too.
if a():
a()
# end
#: E305:8:1
# Example from https://github.com/PyCQA/pycodestyle/issues/400
import stuff
def main():
blah, blah
if __name__ == '__main__':
main()
# end
# E306:3:5
def a():
x = 1
def b():
pass
# end
#: E306:3:5
async def a():
x = 1
def b():
pass
# end
#: E306:3:5 E306:5:9
def a():
x = 2
def b():
x = 1
def c():
pass
# end
# E306:3:5 E306:6:5
def a():
x = 1
class C:
pass
x = 2
def b():
pass
# end
# E306
def foo():
def bar():
pass
def baz(): pass
# end
# E306:3:5
def foo():
def bar(): pass
def baz():
pass
# end
# E306
def a():
x = 2
@decorator
def b():
pass
# end
# E306
def a():
x = 2
@decorator
async def b():
pass
# end
# E306
def a():
x = 2
async def b():
pass
# end

View File

@@ -1,50 +0,0 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ...
class AnoherClass: ...
def a(): ...
@overload
def a(arg: int): ...
@overload
def a(arg: int, name: str): ...
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
@decorated
def with_blank_line(): ...
def ungrouped(): ...
a = "test"
def function_def():
pass
b = "test"
def outer():
def inner():
pass
def inner2():
pass
class Foo: ...
class Bar: ...

View File

@@ -1,4 +0,0 @@
"""Test where the error is after the module's docstring."""
def fn():
pass

View File

@@ -1,4 +0,0 @@
"Test where the first line is a comment, " + "and the rule violation follows it."
def fn():
pass

View File

@@ -1,5 +0,0 @@
def fn1():
pass
def fn2():
pass

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